From e02c1d13d879ce755654b539e379a9fa569cf547 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 24 Aug 2023 11:15:09 +1000 Subject: [PATCH 01/87] chore: Converted SAM Config file from TOML to YAML using samp-cli --- unicorn_contracts/samconfig.toml | 11 ----------- unicorn_contracts/samconfig.yaml | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) delete mode 100644 unicorn_contracts/samconfig.toml create mode 100644 unicorn_contracts/samconfig.yaml diff --git a/unicorn_contracts/samconfig.toml b/unicorn_contracts/samconfig.toml deleted file mode 100644 index 3a937de..0000000 --- a/unicorn_contracts/samconfig.toml +++ /dev/null @@ -1,11 +0,0 @@ -version = 0.1 -[default] -[default.deploy] -[default.deploy.parameters] -disable_rollback = true -stack_name = "uni-prop-local-contract" -s3_prefix = "uni-prop-local-contract" -capabilities = "CAPABILITY_IAM" -parameter_overrides = "Stage=\"Local\"" -resolve_s3 = true -resolve_image_repositories = true \ No newline at end of file diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml new file mode 100644 index 0000000..656e28f --- /dev/null +++ b/unicorn_contracts/samconfig.yaml @@ -0,0 +1,33 @@ +version: "0.1" +default: + global: + parameters: + stack_name: "uni-prop-local-contract" + s3_prefix: "uni-prop-local-contract" + resolve_s3: true + resolve_image_repositories: true + build: + parameters: + cached: true + parallel: true + deploy: + parameters: + disable_rollback: true + confirm_changeset: false + capabilities: "CAPABILITY_IAM" + parameter_overrides: + - "Stage=Local" + package: + parameters: {} + validate: + parameters: + lint: true + sync: + parameters: + watch: true + local_start_api: + parameters: + warm_containers: "EAGER" + local_start_lambda: + parameters: + warm_containers: "EAGER" From 67cce74fe084d16e36fb346aa61c61dd62db1c08 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 24 Aug 2023 11:16:08 +1000 Subject: [PATCH 02/87] feat: Updated template for new Contracts service implementation model --- unicorn_contracts/template.yaml | 421 ++++++++++++++++++++------------ 1 file changed, 263 insertions(+), 158 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 9ed702c..0412ede 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -1,22 +1,24 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 Description: > Unicorn Contracts Service resources. -###################################### -# METADATA -###################################### +#### METADATA Metadata: cfn-lint: config: ignore_checks: - I3042 + - ES1007 + - ES1001 + - ES6000 + - ES4000 -###################################### -# PARAMETERS -###################################### +#### PARAMETERS Parameters: Stage: Type: String @@ -25,10 +27,14 @@ Parameters: - Local - Dev - Prod - -###################################### -# MAPPINGS -###################################### + + ContractsServiceNamespace: + Type: String + Default: "unicorn.contracts" + AllowedValues: + - "unicorn.contracts" + +#### MAPPINGS Mappings: LogsRetentionPeriodMap: Local: @@ -38,21 +44,14 @@ Mappings: Prod: Days: 14 -###################################### -# CONDITIONS -###################################### +#### CONDITIONS Conditions: - IsProd: !Equals - - !Ref Stage - - Prod + IsProd: !Equals [ !Ref Stage, Prod ] -###################################### -# GLOBALS -# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst -###################################### +#### GLOBALS Globals: Api: - OpenApiVersion: 3.0.1 + OpenApiVersion: "3.0.1" Function: Runtime: python3.11 MemorySize: 128 @@ -63,159 +62,127 @@ Globals: Environment: Variables: DYNAMODB_TABLE: !Ref ContractsTable - EVENT_BUS: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" - SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" - POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" - POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default - POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default - POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" # Metric Namespace - LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + SERVICE_NAMESPACE: !Ref ContractsServiceNamespace + POWERTOOLS_SERVICE_NAME: !Ref ContractsServiceNamespace + POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default + POWERTOOLS_LOGGER_LOG_EVENT: !If [ IsProd, "false", "true" ] # Logs incoming event, default + POWERTOOLS_LOGGER_SAMPLE_RATE: !If [ IsProd, "0.1", "0" ] # Debug log sampling percentage, default + POWERTOOLS_METRICS_NAMESPACE: !Ref ContractsServiceNamespace # Metric Namespace + LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage project: AWS Serverless Developer Experience - service: Unicorn Contracts Service + service: !Ref ContractsServiceNamespace -###################################### -# RESOURCES -###################################### +#### RESOURCES Resources: - ###################################### - # LAMBDA FUNCTIONS - ###################################### - CreateContractFunction: - Type: AWS::Serverless::Function + #### SSM PARAMETERS + # Services own and share their namespace + UnicornContractsNamespace: + Type: AWS::SSM::Parameter Properties: - CodeUri: src/ - Handler: contracts_service.create_contract_function.lambda_handler - Policies: - - DynamoDBWritePolicy: - TableName: !Ref ContractsTable - - DynamoDBReadPolicy: - TableName: !Ref ContractsTable - - EventBridgePutEventsPolicy: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" - Events: - CreateContract: - Type: Api - Properties: - Path: /contracts - Method: post - RestApiId: !Ref ContractsApi - - UpdateContractFunction: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsNamespace + Value: !Ref ContractsServiceNamespace + + # Services share their event bus name and arn + UnicornContractsEventBusName: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsEventBus + Value: !GetAtt UnicornContractsEventBus.Name + + UnicornContractsEventBusArn: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsEventBusArn + Value: !GetAtt UnicornContractsEventBus.Arn + + #### LAMBDA FUNCTIONS + ContractEventHandlerFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/ - Handler: contracts_service.update_contract_function.lambda_handler + Handler: contracts_service.contract_event_handler.lambda_handler Policies: - DynamoDBWritePolicy: TableName: !Ref ContractsTable - DynamoDBReadPolicy: TableName: !Ref ContractsTable - - EventBridgePutEventsPolicy: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" Events: - UpdateContract: - Type: Api + IngestQueue: + Type: SQS Properties: - Path: /contracts - Method: put - RestApiId: !Ref ContractsApi + Queue: !GetAtt ContractServiceIngestQueue.Arn + BatchSize: 1 + Enabled: true + ScalingConfig: + MaximumConcurrency: 5 + + ContractEventHandlerFunctionLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" + RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] - ###################################### - # API GATEWAY REST API - ###################################### + #### API GATEWAY REST API ContractsApi: Type: AWS::Serverless::Api DependsOn: ContractsApiGwAccountConfig + UpdateReplacePolicy: Delete + DeletionPolicy: Delete Properties: StageName: !Ref Stage - EndpointConfiguration: + EndpointConfiguration: Type: REGIONAL TracingEnabled: true MethodSettings: - MetricsEnabled: true ResourcePath: /* HttpMethod: "*" - LoggingLevel: !If - - IsProd - - ERROR - - INFO + LoggingLevel: !If [ IsProd, ERROR, INFO ] ThrottlingBurstLimit: 10 ThrottlingRateLimit: 100 AccessLogSetting: DestinationArn: !GetAtt ContractsApiLogGroup.Arn - Format: > - {"requestId":"$context.requestId", - "integration-error":"$context.integration.error", - "integration-status":"$context.integration.status", - "integration-latency":"$context.integration.latency", - "integration-requestId":"$context.integration.requestId", - "integration-integrationStatus":"$context.integration.integrationStatus", - "response-latency":"$context.responseLatency", - "status":"$context.status"} + Format: !ToJsonString + requestId: $context.requestId + integration-error: $context.integration.error + integration-status: $context.integration.status + integration-latency: $context.integration.latency + integration-requestId: $context.integration.requestId + integration-integrationStatus: $context.integration.integrationStatus + response-latency: $context.responseLatency + status: $context.status + DefinitionBody: + "Fn::Transform": + Name: "AWS::Include" + Parameters: + Location: 'api.yaml' Tags: stage: !Ref Stage project: AWS Serverless Developer Experience - service: Unicorn Contracts Service - + namespace: !Ref ContractsServiceNamespace + ContractsApiGwAccountConfig: Type: AWS::ApiGateway::Account Properties: - CloudWatchRoleArn: !GetAtt ContractsApiAccessLogsRole.Arn + CloudWatchRoleArn: !GetAtt ContractsApiGwAccountConfigRole.Arn - ###################################### - # CLOUDWATCH LOG GROUPS - ###################################### + # API GW Cloudwatch Log Group ContractsApiLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days - - CreateContractFunctionLogGroup: - Type: AWS::Logs::LogGroup - DeletionPolicy: Delete - UpdateReplacePolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${CreateContractFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days - - UpdateContractFunctionLogGroup: - Type: AWS::Logs::LogGroup - DeletionPolicy: Delete - UpdateReplacePolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${UpdateContractFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days - - ###################################### - # IAM ROLES - ###################################### - ContractsApiAccessLogsRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: apigateway.amazonaws.com - ManagedPolicyArns: - - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] - ApiInvokeLambdaRole: + # API GW IAM roles + ContractsApiIntegrationRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -225,52 +192,190 @@ Resources: Principal: Service: apigateway.amazonaws.com Policies: - - PolicyName: root + - PolicyName: AllowSqsIntegration PolicyDocument: Statement: - Effect: Allow Action: - - lambda:InvokeFunction - Resource: - - !GetAtt CreateContractFunction.Arn - - !GetAtt UpdateContractFunction.Arn + - 'sqs:SendMessage' + - 'sqs:GetQueueUrl' + Resource: !GetAtt ContractServiceIngestQueue.Arn + + ContractsApiGwAccountConfigRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - apigateway.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs - ###################################### - # DYNAMODB TABLE - ###################################### + #### INGEST QUEUES + ContractServiceIngestQueue: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub ContractServiceIngestQueue-${Stage} + RedrivePolicy: + deadLetterTargetArn: !GetAtt ContractServiceIngestDLQ.Arn + maxReceiveCount: 1 + VisibilityTimeout: 20 + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: AWS Serverless Developer Experience + - Key: namespace + Value: !Ref ContractsServiceNamespace + + ContractServiceIngestDLQ: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub ContractServiceIngestDLQ-${Stage} + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: AWS Serverless Developer Experience + - Key: namespace + Value: !Ref ContractsServiceNamespace + + #### DYNAMODB TABLE ContractsTable: - Type: AWS::Serverless::SimpleTable + Type: AWS::DynamoDB::Table + UpdateReplacePolicy: Delete DeletionPolicy: Delete + Properties: + AttributeDefinitions: + - AttributeName: property_id # OR PropertyId? + AttributeType: S + KeySchema: + - AttributeName: property_id # OR PropertyId? + KeyType: HASH + StreamSpecification: + StreamViewType: NEW_AND_OLD_IMAGES + BillingMode: PAY_PER_REQUEST + Tags: + - Key: project + Value: AWS Serverless Developer Experience + - Key: namespace + Value: !Ref ContractsServiceNamespace + - Key: stage + Value: !Ref Stage + + #### EVENT BUS + # Event bus for Contract Service used to publish and consume events + UnicornContractsEventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Sub UnicornContractsBus-${Stage} + + # Event bus policy to restrict who can publish events (should only be services from ContractsServiceNamespace) + ContractEventsBusPublishPolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Ref UnicornContractsEventBus + StatementId: !Sub ContactsPublishEventsPolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: events:PutEvents + Resource: !GetAtt UnicornContractsEventBus.Arn + Condition: + StringEquals: + events:source: + - !Ref ContractsServiceNamespace + + # Catchall rule used for development purposes. + UnicornContractsCatchAllRule: + Type: AWS::Events::Rule + Properties: + Name: contracts.catchall + Description: Catch all events published by the contracts service. + EventBusName: !Ref UnicornContractsEventBus + EventPattern: + account: + - !Ref AWS::AccountId + source: + - !Ref ContractsServiceNamespace + State: ENABLED #You may want to disable this rule in production + Targets: + - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn + Id: !Sub UnicornContractsCatchAllLogGroupTarget-${Stage} + + # CloudWatch log group used to catch all events + UnicornContractsCatchAllLogGroup: + Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete + DeletionPolicy: Delete Properties: - PrimaryKey: - Name: property_id - Type: String + LogGroupName: !Sub /aws/events/${Stage}/${ContractsServiceNamespace}-catchall + RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] + # Permissions to allow EventBridge to send logs to CloudWatch + EventBridgeCloudWatchLogGroupPolicy: + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub EventBridgeToCWLogsPolicy-${Stage} + # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument + PolicyDocument: !Sub + - | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "delivery.logs.amazonaws.com", + "events.amazonaws.com" + ] + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "${contractsServiceLogGroup}" + ] + } + ] + } + - contractsServiceLogGroup: UnicornContractsCatchAllLogGroup.Arn -###################################### -# OUTPUTS -###################################### +#### OUTPUTS Outputs: + BaseUrl: + Description: Web service API endpoint + Value: !Sub "https://${ContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" ApiUrl: Description: Contract service API endpoint Value: !Sub "https://${ContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + IngestQueueUrl: + Description: URL for the Ingest SQS Queue + Value: !GetAtt ContractServiceIngestQueue.QueueUrl + ContractsTableName: + Description: DynamoDB table storing contract information Value: !Ref ContractsTable - CreateContractFunction: - Value: !GetAtt CreateContractFunction.Arn - - UpdateContractFunction: - Value: !GetAtt UpdateContractFunction.Arn + ContractEventHandlerFunctionArn: + Description: Create Contract function ARN + Value: !GetAtt ContractEventHandlerFunction.Arn IsProd: Description: Is Production? - Value: !If - - IsProd - - 'true' - - 'false' - - BaseUrl: - Description: Web service API endpoint - Value: !Sub "https://${ContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" + Value: !If [ IsProd, 'true', 'false' ] From 329ebed8d7d55679fdf6b81ffb1d2a7457d63f3a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 24 Aug 2023 15:33:22 +1000 Subject: [PATCH 03/87] chore: add API contract --- unicorn_contracts/api.yaml | 145 +++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 unicorn_contracts/api.yaml diff --git a/unicorn_contracts/api.yaml b/unicorn_contracts/api.yaml new file mode 100644 index 0000000..aee71d4 --- /dev/null +++ b/unicorn_contracts/api.yaml @@ -0,0 +1,145 @@ +openapi: "3.0.1" +info: + title: "Unicorn Contracts API" + version: "1.0.0" + description: Unicorn Properties Contract Service API +paths: + /contract: + post: + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateContractModel" + required: true + responses: + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-request-validator: "Validate body" + x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + httpMethod: "POST" + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${ContractServiceIngestQueue.QueueName}" + responses: + default: + statusCode: "200" + responseTemplates: + application/json: "{\"message\":\"OK\"}" + requestParameters: + integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" + requestTemplates: + application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" + passthroughBehavior: "never" + type: "aws" + options: + responses: + "200": + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + responseParameters: + method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Origin: "'*'" + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: "when_no_match" + type: "mock" + put: + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateContractModel" + required: true + responses: + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-request-validator: "Validate body" + x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + httpMethod: "POST" + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${ContractServiceIngestQueue.QueueName}" + responses: + default: + statusCode: "200" + responseTemplates: + application/json: "{\"message\":\"OK\"}" + requestParameters: + integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" + requestTemplates: + application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" + passthroughBehavior: "never" + type: "aws" +components: + schemas: + CreateContractModel: + required: + - "property_id" + - "seller_name" + - "address" + type: "object" + properties: + property_id: + type: "string" + seller_name: + type: "string" + address: + required: + - "city" + - "country" + - "number" + - "street" + type: "object" + properties: + country: + type: "string" + city: + type: "string" + street: + type: "string" + number: + type: "integer" + UpdateContractModel: + required: + - "property_id" + type: "object" + properties: + $ref: "#/components/schemas/CreateContractModel/properties" + # property_id: + # type: "string" + Empty: + title: "Empty Schema" + type: "object" +x-amazon-apigateway-request-validators: + Validate body: + validateRequestParameters: false + validateRequestBody: true From 4511b41abe33e8e340884607fc74d0c6d604992b Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 24 Aug 2023 16:08:20 +1000 Subject: [PATCH 04/87] chore: Refactored integration tests --- unicorn_contracts/Makefile | 6 +- unicorn_contracts/poetry.lock | 204 +++++------------- unicorn_contracts/pyproject.toml | 11 +- unicorn_contracts/samconfig.yaml | 3 +- .../contract_event_handler.py | 96 +++++++++ .../create_contract_function.py | 135 ------------ .../{contract_status.py => enums.py} | 3 +- .../src/contracts_service/exceptions.py | 18 +- .../src/contracts_service/helper.py | 127 ----------- .../update_contract_function.py | 191 ---------------- .../tests/events/contract_status_changed.json | 17 -- unicorn_contracts/tests/events/event.json | 62 ------ .../tests/events/put_events.json | 14 -- .../tests/integration/__init__.py | 44 ++++ .../create_contract_invalid_payload_1.json | 5 + .../create_contract_valid_payload_1.json | 10 + ...e_existing_contract_invalid_payload_1.json | 6 + ...pdate_existing_contract_valid_payload.json | 3 + ...te_missing_contract_invalid_payload_1.json | 5 + ...update_missing_contract_valid_payload.json | 3 + .../integration/test_create_contract_apigw.py | 73 ++----- .../integration/test_update_contract_apigw.py | 98 +++------ 22 files changed, 295 insertions(+), 839 deletions(-) create mode 100644 unicorn_contracts/src/contracts_service/contract_event_handler.py delete mode 100644 unicorn_contracts/src/contracts_service/create_contract_function.py rename unicorn_contracts/src/contracts_service/{contract_status.py => enums.py} (98%) delete mode 100644 unicorn_contracts/src/contracts_service/helper.py delete mode 100644 unicorn_contracts/src/contracts_service/update_contract_function.py delete mode 100644 unicorn_contracts/tests/events/contract_status_changed.json delete mode 100644 unicorn_contracts/tests/events/event.json delete mode 100644 unicorn_contracts/tests/events/put_events.json create mode 100644 unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json create mode 100644 unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json create mode 100644 unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json create mode 100644 unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json create mode 100644 unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json create mode 100644 unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json diff --git a/unicorn_contracts/Makefile b/unicorn_contracts/Makefile index f1cbbf2..a942d09 100644 --- a/unicorn_contracts/Makefile +++ b/unicorn_contracts/Makefile @@ -1,7 +1,9 @@ stackName := uni-prop-local-contract +ci: clean build deploy + build: - cfn-lint template.yaml -a cfn_lint_serverless.rules + # cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt sam build -c $(DOCKER_OPTS) @@ -20,7 +22,7 @@ unit-test: poetry run pytest tests/unit/ integration-test: deps - AWS_SAM_STACK_NAME=$(stackName) poetry run pytest tests/integration/ + poetry run pytest tests/integration/ logs: sam logs --stack-name $(stackName) -t diff --git a/unicorn_contracts/poetry.lock b/unicorn_contracts/poetry.lock index d9a61ac..ff73313 100644 --- a/unicorn_contracts/poetry.lock +++ b/unicorn_contracts/poetry.lock @@ -2,17 +2,17 @@ [[package]] name = "aws-lambda-powertools" -version = "2.22.0" +version = "2.23.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.22.0-py3-none-any.whl", hash = "sha256:eae1f1c961893dab5d1e75ffb44d9b58f6426cb148aa39413b04cf36ae46fbe3"}, - {file = "aws_lambda_powertools-2.22.0.tar.gz", hash = "sha256:0fd535251454b1bd68dbff65e3ed56aa567f3841011e2afbd557b125596a6814"}, + {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, + {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, ] [package.dependencies] -aws-xray-sdk = {version = ">=2.8.0,<3.0.0", optional = true, markers = "extra == \"tracer\" or extra == \"all\""} +boto3 = {version = ">=1.20.32,<2.0.0", optional = true, markers = "extra == \"aws-sdk\""} typing-extensions = ">=4.6.2,<5.0.0" [package.extras] @@ -39,17 +39,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.15" +version = "1.28.33" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.15-py3-none-any.whl", hash = "sha256:84b7952858e9319968b0348d9894a91a6bb5f31e81a45c68044d040a12362abe"}, - {file = "boto3-1.28.15.tar.gz", hash = "sha256:a6e711e0b6960c3a5b789bd30c5a18eea7263f2a59fc07f85efa5e04804e49d2"}, + {file = "boto3-1.28.33-py3-none-any.whl", hash = "sha256:07997e299e7b87afbbb25dc9de677017eafbd96b4f1b81e931d5127716dc6dd1"}, + {file = "boto3-1.28.33.tar.gz", hash = "sha256:fafc0eda7ebe7878be2ab934558ea1776cbd1bd624ce9e9b827e304d301ccd00"}, ] [package.dependencies] -botocore = ">=1.31.15,<1.32.0" +botocore = ">=1.31.33,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +58,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.15" +version = "1.31.33" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.15-py3-none-any.whl", hash = "sha256:b3a0f787f275711875476cbe12a0123b2e6570b2f505e2fa509dcec3c5410b57"}, - {file = "botocore-1.31.15.tar.gz", hash = "sha256:b46d1ce4e0cf42d28fdf61ce0c999904645d38b51cb809817a361c0cec16d487"}, + {file = "botocore-1.31.33-py3-none-any.whl", hash = "sha256:1b76549c45f712ca9734888e60a2ab9c857e6e6025b156b36c344162a7e9d0dc"}, + {file = "botocore-1.31.33.tar.gz", hash = "sha256:3fd7cb89cf834b28bc7e8427cb29bb861b10652a3bebe9d0d18d9a2c1e4f3f67"}, ] [package.dependencies] @@ -257,108 +257,36 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "coverage" -version = "7.2.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, -] - -[package.extras] -toml = ["tomli"] - [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -504,13 +432,13 @@ files = [ [[package]] name = "moto" -version = "4.1.13" +version = "4.2.0" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.1.13-py2.py3-none-any.whl", hash = "sha256:9650d05d89b6f97043695548fbc0d8fb293f4177daaebbcee00bb0d171367f1a"}, - {file = "moto-4.1.13.tar.gz", hash = "sha256:dd3e2ad920ab8b058c4f62fa7c195b788bd1f018cc701a1868ff5d5c4de6ed47"}, + {file = "moto-4.2.0-py2.py3-none-any.whl", hash = "sha256:e4835912f05627b6a53b938562b717122230fb038d023819133f8526f60ed0a7"}, + {file = "moto-4.2.0.tar.gz", hash = "sha256:00fbae396fc48c3596e47b4e3267c1a41ca01c968de023beb68e774c63910b58"}, ] [package.dependencies] @@ -525,26 +453,28 @@ werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" xmltodict = "*" [package.extras] -all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] apigateway = ["PyYAML (>=5.1)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.2.8)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] apigatewayv2 = ["PyYAML (>=5.1)"] appsync = ["graphql-core"] awslambda = ["docker (>=3.0.0)"] batch = ["docker (>=3.0.0)"] -cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] cognitoidp = ["ecdsa (!=0.15)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] ds = ["sshpubkeys (>=3.1.0)"] -dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] -dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] ebs = ["sshpubkeys (>=3.1.0)"] ec2 = ["sshpubkeys (>=3.1.0)"] efs = ["sshpubkeys (>=3.1.0)"] eks = ["sshpubkeys (>=3.1.0)"] glue = ["pyparsing (>=3.0.7)"] iotdata = ["jsondiff (>=1.1.2)"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "sshpubkeys (>=3.1.0)"] route53resolver = ["sshpubkeys (>=3.1.0)"] -s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.3)"] -server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.6)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.3.6)"] +server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] ssm = ["PyYAML (>=5.1)"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] @@ -605,24 +535,6 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - [[package]] name = "pytest-mock" version = "3.11.1" @@ -726,33 +638,33 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.23.1" +version = "0.23.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, - {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, ] [package.dependencies] pyyaml = "*" -requests = ">=2.22.0,<3.0" +requests = ">=2.30.0,<3.0" types-PyYAML = "*" -urllib3 = ">=1.25.10" +urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] [[package]] name = "s3transfer" -version = "0.6.1" +version = "0.6.2" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.7" files = [ - {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, - {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, + {file = "s3transfer-0.6.2-py3-none-any.whl", hash = "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084"}, + {file = "s3transfer-0.6.2.tar.gz", hash = "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"}, ] [package.dependencies] @@ -812,13 +724,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "werkzeug" -version = "2.3.6" +version = "2.3.7" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, - {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, + {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"}, + {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"}, ] [package.dependencies] @@ -940,4 +852,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d0163f802a4318ec92b5273a56c206db96edfd7f4b8e88165e0e47056e62cdfa" +content-hash = "175a485c562e962bc87f676afaab46d70858d5fa79a2650f9c3b6fa429ce683a" diff --git a/unicorn_contracts/pyproject.toml b/unicorn_contracts/pyproject.toml index 89561a3..483d349 100644 --- a/unicorn_contracts/pyproject.toml +++ b/unicorn_contracts/pyproject.toml @@ -9,18 +9,19 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.15" -aws-lambda-powertools = {extras = ["tracer"], version = "^2.22.0"} +boto3 = "^1.28.32" +aws-lambda-powertools = {extras = ["aws-sdk"], version = "^2.23.0"} aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" pytest-mock = "^3.11.1" -pytest-cov = "^4.1.0" -coverage = "^7.2.7" +# pytest-cov = "^4.1.0" +# coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" +pyyaml = "^6.0.1" [build-system] requires = ["poetry-core>=1.0.0"] @@ -28,7 +29,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -vv -W ignore::UserWarning --cov=contracts_service --cov-config=.coveragerc --cov-report term --cov-report html" +addopts = "-ra -vv -W ignore::UserWarning" testpaths = [ "./tests/unit", "./tests/integration", diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 656e28f..ae588c2 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -1,4 +1,4 @@ -version: "0.1" +version: 0.1 default: global: parameters: @@ -14,6 +14,7 @@ default: parameters: disable_rollback: true confirm_changeset: false + fail_on_empty_changeset: false capabilities: "CAPABILITY_IAM" parameter_overrides: - "Stage=Local" diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py new file mode 100644 index 0000000..78b8c7d --- /dev/null +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -0,0 +1,96 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import os + +import boto3 +from aws_lambda_powertools.logging import Logger +from aws_lambda_powertools.metrics import Metrics +from aws_lambda_powertools.tracing import Tracer +from aws_lambda_powertools.utilities.data_classes import event_source, SQSEvent +from aws_lambda_powertools.utilities.typing import LambdaContext + +# from contracts_service.enums import ContractStatus + + +# Initialise Environment variables +if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: + raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") +if (DYNAMODB_TABLE := os.environ.get("DYNAMODB_TABLE")) is None: + raise EnvironmentError("DYNAMODB_TABLE environment variable is undefined") + +# Initialise PowerTools +logger: Logger = Logger() +tracer: Tracer = Tracer() +metrics: Metrics = Metrics() + +# Initialise boto3 clients +dynamodb = boto3.resource("dynamodb") +table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore + + +@metrics.log_metrics(capture_cold_start_metric=True) # type: ignore +@logger.inject_lambda_context() +@tracer.capture_method +@event_source(data_class=SQSEvent) +def lambda_handler(event: SQSEvent, context: LambdaContext): + # Multiple records can be delivered in a single event + for record in event.records: + match record.message_attributes.get('HttpMethod'): + case 'POST': + create_contract(record.json_body) + case 'PUT': + update_contract(record.json_body) + case other: + raise Exception(f'Unable to handle HttpMethod {other}') + + +@tracer.capture_method +def create_contract(contract: dict) -> None: + """Create contract inside DynamoDB table + + Parameters + ---------- + contract (dict): _description_ + + Returns + ------- + dict + DynamoDB put Item response + """ + logger.info(contract) + + # if contract id exists: + # if constract status is APPROVED | DRAFT: + # log message + # return + # create with status = DRAFT + # return + + # return table.put_item(Item=contract,) + + +@tracer.capture_method +def update_contract(contract: dict) -> None: + """Update an existing contract inside DynamoDB table + + Parameters + ---------- + contract (dict): _description_ + + Returns + ------- + dict + DynamoDB put Item response + """ + logger.info(contract) + + # if contract doesnt exist + # lod message + # return + # if contract status in [ APPROVED | CANCELLED | CLOSED | EXPIRED ] + # close + # return + # update contract status to APPROVED + + # return table.put_item(Item=contract,) diff --git a/unicorn_contracts/src/contracts_service/create_contract_function.py b/unicorn_contracts/src/contracts_service/create_contract_function.py deleted file mode 100644 index a822203..0000000 --- a/unicorn_contracts/src/contracts_service/create_contract_function.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -import json -import os -import uuid - -import boto3 -from aws_lambda_powertools.logging import Logger, correlation_paths -from aws_lambda_powertools.metrics import Metrics -from aws_lambda_powertools.tracing import Tracer - -from contracts_service.contract_status import ContractStatus -from contracts_service.exceptions import EventValidationException -from contracts_service.helper import get_current_date, get_event_body, publish_event - -# Initialise Environment variables -if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: - raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") -if (DYNAMODB_TABLE := os.environ.get("DYNAMODB_TABLE")) is None: - raise EnvironmentError("DYNAMODB_TABLE environment variable is undefined") - -# Initialise PowerTools -logger: Logger = Logger() -tracer: Tracer = Tracer() -metrics: Metrics = Metrics() - -# Initialise boto3 clients -dynamodb = boto3.resource("dynamodb") -table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore -event_bridge = boto3.client("events") - - -@metrics.log_metrics(capture_cold_start_metric=True) # type: ignore -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST, log_event=True) # type: ignore -@tracer.capture_method -def lambda_handler(event, context): - """Lambda handler for new_contract. - - Parameters - ---------- - event : API Gateway Lambda Proxy Request - The event passed to the function. - context : AWS Lambda Context - The context for the Lambda function. - - Returns - ------- - API Gateway Lambda Proxy Response - HTTP response object with Contract and Property ID - """ - # Get contract and property details from the event - try: - event_json = validate_event(event) - except EventValidationException as ex: - return ex.apigw_return - - # Create new Contract - current_date: str = get_current_date(context.aws_request_id) - - contract = { - "property_id": event_json["property_id"], # PK - "contact_created": current_date, - "contract_last_modified_on": current_date, - "contract_id": str(uuid.uuid4()), - "address": event_json["address"], - "seller_name": event_json["seller_name"], - "contract_status": ContractStatus.DRAFT.name, - } - - # create entry in DDB for new contract - create_contract(contract) - - # Annotate trace with contract status - tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) - - # Publish ContractStatusChanged event - publish_event(contract, context.aws_request_id) - - # return generated contract ID back to user: - return { - "statusCode": 200, - "body": json.dumps(contract) - } - - -@tracer.capture_method -def create_contract(contract) -> dict: - """Create contract inside DynamoDB table - - Parameters - ---------- - contract (dict): _description_ - - Returns - ------- - dict - DynamoDB put Item response - """ - # TODO: create entry in DDB for new contract - return table.put_item(Item=contract,) - - -@tracer.capture_method -def validate_event(event): - """Validates the body of the API Gateway event - - Parameters - ---------- - event : dict - API Gateway event - - Returns - ------- - dict - The body of the API - - Raises - ------ - EventValidationException - The ``Raises`` section is a list of all exceptions - that are relevant to the interface. - """ - - try: - event_json = get_event_body(event) - except Exception as ex: - logger.exception(ex) - raise EventValidationException() from ex - - for i in ["property_id", "address", "seller_name"]: - if i not in event_json.keys(): - raise EventValidationException() - - return event_json diff --git a/unicorn_contracts/src/contracts_service/contract_status.py b/unicorn_contracts/src/contracts_service/enums.py similarity index 98% rename from unicorn_contracts/src/contracts_service/contract_status.py rename to unicorn_contracts/src/contracts_service/enums.py index 3072cee..fa2b4d7 100644 --- a/unicorn_contracts/src/contracts_service/contract_status.py +++ b/unicorn_contracts/src/contracts_service/enums.py @@ -3,6 +3,7 @@ from enum import Enum + class ContractStatus(Enum): """Contract status Enum @@ -21,4 +22,4 @@ class ContractStatus(Enum): CANCELLED = 2 CLOSED = 3 DRAFT = 4 - EXPIRED= 5 + EXPIRED = 5 diff --git a/unicorn_contracts/src/contracts_service/exceptions.py b/unicorn_contracts/src/contracts_service/exceptions.py index d8b20c9..17051ad 100644 --- a/unicorn_contracts/src/contracts_service/exceptions.py +++ b/unicorn_contracts/src/contracts_service/exceptions.py @@ -3,6 +3,7 @@ import json + class ContractNotFoundException(Exception): """ Custom exception for encapsulating exceptions for Lambda handler @@ -19,20 +20,3 @@ def __init__(self, message=None, status_code=None, details=None): "statusCode": self.status_code, "body": json.dumps({"message": self.message}) } - -class EventValidationException(Exception): - """ - Custom exception for events that have failed validation - """ - - def __init__(self, message=None, status_code=None, details=None): - super(EventValidationException, self).__init__() - - self.message = message or "Event body not valid." - self.status_code = status_code or 400 - self.details = details or {} - - self.apigw_return = { - "statusCode": self.status_code, - "body": json.dumps({"message": self.message}) - } diff --git a/unicorn_contracts/src/contracts_service/helper.py b/unicorn_contracts/src/contracts_service/helper.py deleted file mode 100644 index 53c29f5..0000000 --- a/unicorn_contracts/src/contracts_service/helper.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -import os -import json -from datetime import datetime - -import boto3 -from aws_lambda_powertools.tracing import Tracer -from aws_lambda_powertools.logging import Logger - -from contracts_service.exceptions import EventValidationException - -# Initialise Environment variables -if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: - raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") - -if (EVENT_BUS := os.environ.get("EVENT_BUS")) is None: - raise EnvironmentError("EVENT_BUS environment variable is undefined") - -# Initialise PowerTools -logger: Logger = Logger(service="helperFunc") -tracer: Tracer = Tracer(service="helperFunc") - -# Initialise boto3 clients -event_bridge = boto3.client('events') - -request_dates = {} - -@tracer.capture_method -def get_current_date(request_id=None): - """Return current date for this invocation. To keep the return value consistent while function is - queried multiple times, function maintains the value returned for each request id and returns same - value on subsequent requests. - - Parameters - ---------- - request_id : str - context.aws_request_id - - Returns - ------- - str - Current date time i.e. '01/08/2022 20:36:30' - """ - if request_id is not None and request_id in request_dates.keys(): - return request_dates[request_id] - - now_str = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - logger.info(f"Time recorded: {now_str} for request {request_id}") - request_dates[request_id] = now_str - return now_str - - -@tracer.capture_method -def get_event_body(event): - """Get body of the API Gateway event - - Parameters - ---------- - event : dict - API Gateway event - - Returns - ------- - dict - The body of the API - - Raises - ------ - EventValidationException - The ``Raises`` section is a list of all exceptions - that are relevant to the interface. - """ - event_body = event.get("body", '{}') - - try: - event_json = json.loads(event_body) - except json.decoder.JSONDecodeError as e: - logger.critical("This event input is not a valid JSON") - raise e - except TypeError as e: - logger.critical("This event input is not a valid JSON") - raise e - - # Check if event body contains data, otherwise log & raise exception - if not event_json: - msg = "This event input did not contain body payload." - logger.critical(msg) - raise EventValidationException(msg) - - return event_json - - -@tracer.capture_method -def publish_event(contract, request_id): - """Push contract event data to EventBridge bus - - Parameters - ---------- - contract : dict - Contract object - - Returns - ------- - Amazon EventBridge PutEvents response : dict - response object from EventBridge API call - """ - - contract_status_changed_event = { - "contract_last_modified_on": contract["contract_last_modified_on"], - "property_id": contract["property_id"], - "contract_id": contract["contract_id"], - "contract_status": contract["contract_status"], - } - - return event_bridge.put_events( - Entries=[ - { - 'Time': get_current_date(request_id), - "Source": SERVICE_NAMESPACE, - "DetailType": "ContractStatusChanged", - "Detail": json.dumps(contract_status_changed_event), - "EventBusName": EVENT_BUS - } - ] - ) diff --git a/unicorn_contracts/src/contracts_service/update_contract_function.py b/unicorn_contracts/src/contracts_service/update_contract_function.py deleted file mode 100644 index dc342f6..0000000 --- a/unicorn_contracts/src/contracts_service/update_contract_function.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -import json -import os - -import boto3 -from aws_lambda_powertools.logging import Logger -from aws_lambda_powertools.metrics import Metrics -from aws_lambda_powertools.tracing import Tracer -from aws_lambda_powertools.logging import correlation_paths -from botocore.exceptions import ClientError - -from contracts_service.contract_status import ContractStatus -from contracts_service.exceptions import (ContractNotFoundException, - EventValidationException) -from contracts_service.helper import get_current_date, get_event_body, publish_event - -# Initialise Environment variables -if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: - raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") - -if (DYNAMODB_TABLE := os.environ.get("DYNAMODB_TABLE")) is None: - raise EnvironmentError("DYNAMODB_TABLE environment variable is undefined") - -# Initialise PowerTools -logger: Logger = Logger() -tracer: Tracer = Tracer() -metrics: Metrics = Metrics() - -# Initialise boto3 clients -dynamodb = boto3.resource('dynamodb') -table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore - - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST, log_event=True) # type: ignore -@metrics.log_metrics(capture_cold_start_metric=True) # type: ignore -@tracer.capture_method -def lambda_handler(event, context): - """Lambda handler for updating contract information - - Parameters - ---------- - event : dict - Amazon API Gateway event - context : dict - AWS Lambda context - - Returns - ------- - dict - HTTP response - """ - - # Get contract and property details from the event - try: - event_json = validate_event(event) - except EventValidationException as ex: - return ex.apigw_return - - # Load existing contract - try: - existing_contract = get_existing_contract(event_json["property_id"]) - logger.info({"Found existing contract": existing_contract }) - except ContractNotFoundException as ex: - logger.info({"Contract not found!"}) - return ex.apigw_return - - # Set current date - current_date = get_current_date(context.aws_request_id) - - # define contract with approved status - contract = { - "contract_id": existing_contract['contract_id'], - "property_id": existing_contract["property_id"], - "contract_last_modified_on": current_date, - "contract_status": ContractStatus.APPROVED.name, - } - - # Update DDB entry - update_contract(contract) - - # Annotate trace with contract status - tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) - - # Publish ContractStatusChanged event - publish_event(contract, context.aws_request_id) - - return { - "statusCode": 200, - "body": json.dumps(contract) - } - -@tracer.capture_method -def update_contract(contract) -> dict: - """Update contract inside DynamoDB table - - Args: - contract (dict): _description_ - - Returns: - dict: _description_ - """ - - logger.info(msg={"Updating contract": contract}) - - try: - response = table.update_item( - TableName=DYNAMODB_TABLE, - Key={ - 'property_id': contract['property_id'], - }, - UpdateExpression="set contract_status=:t, modified_date=:m", - ExpressionAttributeValues={ - ':t': contract['contract_status'], - ':m': contract['contract_last_modified_on'] - }, - ReturnValues="UPDATED_NEW" - ) - - return response["Attributes"] - - except ClientError as error: - if error.response["Error"]["Code"] == "ResourceNotFoundException": - logger.exception("Error updating Contract status.") - raise error - - -@tracer.capture_method -def get_existing_contract(property_id: str) -> dict: - """Returns Contract for a specified property - - Parameters - ---------- - property_id : str - Property ID - - Returns - ------- - dict - Contract info - """ - - try: - response = table.get_item( - Key={ - 'property_id': property_id - } - ) - return response["Item"] - - except ClientError as error: - if error.response["Error"]["Code"] == "ResourceNotFoundException": - logger.exception("Error getting contract.") - raise ContractNotFoundException() from error - raise error - except KeyError as _: - raise ContractNotFoundException() from _ - - -@tracer.capture_method -def validate_event(event): - """Validates the body of the API Gateway event - - Parameters - ---------- - event : dict - API Gateway event - - Returns - ------- - dict - The body of the API - - Raises - ------ - EventValidationException - The ``Raises`` section is a list of all exceptions - that are relevant to the interface. - """ - try: - event_json = get_event_body(event) - except Exception as ex: - logger.exception(ex) - raise EventValidationException() from ex - - for i in ["property_id"]: - if i not in event_json.keys(): - raise EventValidationException() - - return event_json diff --git a/unicorn_contracts/tests/events/contract_status_changed.json b/unicorn_contracts/tests/events/contract_status_changed.json deleted file mode 100644 index 9bb665a..0000000 --- a/unicorn_contracts/tests/events/contract_status_changed.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0", - "account": "123456789012", - "region": "us-east-1", - "detail-type": "Application Insights Problem Update", - "source": "unicorn.contracts", - "time": "2022-08-14T22:06:31Z", - "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", - "resources": [ - ], - "detail": { - "contract_updated_on": "10/08/2022 19:56:30", - "contract_id": 222, - "property_id": "bbb", - "contract_status": "DRAFT" - } -} diff --git a/unicorn_contracts/tests/events/event.json b/unicorn_contracts/tests/events/event.json deleted file mode 100644 index b886a69..0000000 --- a/unicorn_contracts/tests/events/event.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "body": "{\"Address\":\"St.1 , Building 10\",\"SellerName\":\"John Smith\",\"PropertyId\":\"4781231c-bc30-4f30-8b30-7145f4dd1adb\"}", - "resource": "/contract", - "path": "/", - "httpMethod": "POST", - "isBase64Encoded": false, - "queryStringParameters": { - "foo": "bar" - }, - "pathParameters": { - "proxy": "/path/to/resource" - }, - "stageVariables": { - "baz": "qux" - }, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, sdch", - "Accept-Language": "en-US,en;q=0.8", - "Cache-Control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "1234567890.execute-api.us-east-1.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Custom User Agent String", - "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", - "X-Forwarded-For": "127.0.0.1, 127.0.0.2", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "requestContext": { - "accountId": "123456789012", - "resourceId": "123456", - "stage": "prod", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "requestTime": "09/Apr/2015:12:34:56 +0000", - "requestTimeEpoch": 1428582896000, - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "accessKey": null, - "sourceIp": "127.0.0.1", - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Custom User Agent String", - "user": null - }, - "path": "/prod/hello", - "resourcePath": "/hello", - "httpMethod": "POST", - "apiId": "1234567890", - "protocol": "HTTP/1.1" - } -} diff --git a/unicorn_contracts/tests/events/put_events.json b/unicorn_contracts/tests/events/put_events.json deleted file mode 100644 index 30fe8b9..0000000 --- a/unicorn_contracts/tests/events/put_events.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "Source": "unicorn.contracts", - "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": 111,\"property_id\":\"aaa\",\"contract_status\":\"DRAFT\"}", - "DetailType": "ContractStatusChanged", - "EventBusName": "Dev-UnicornPropertiesEventBus" - }, - { - "Source": "unicorn.contracts", - "Detail": "{\"contract_updated_on\":\"10/08/2022 19:56:30\",\"contract_id\":222,\"property_id\":\"bbb\",\"contract_status\":\"DRAFT\"}", - "DetailType": "ContractStatusChanged", - "EventBusName": "Dev-UnicornPropertiesEventBus" - } -] diff --git a/unicorn_contracts/tests/integration/__init__.py b/unicorn_contracts/tests/integration/__init__.py index e69de29..1cc6bfd 100644 --- a/unicorn_contracts/tests/integration/__init__.py +++ b/unicorn_contracts/tests/integration/__init__.py @@ -0,0 +1,44 @@ +from pathlib import Path + +from yaml import load +# From: https://pyyaml.org/wiki/PyYAMLDocumentation +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + +import boto3 + + +DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' +STACK_OUTPUTS = dict() + +cfn = boto3.client("cloudformation") + + +def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: + with open(samconfig, 'r') as f: + conf = load(f, Loader=Loader) + stack_name = conf['default']['global']['parameters']['stack_name'] + + return stack_name + + +def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: + """ + Get the value of an output + """ + + if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): + try: + response = cfn.describe_stacks(StackName=stack_name) + except Exception as e: + raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e + + outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} + STACK_OUTPUTS[stack_name] = outputs + + try: + return outputs[output_name] + except KeyError as e: + raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e diff --git a/unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json new file mode 100644 index 0000000..1f352a3 --- /dev/null +++ b/unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json @@ -0,0 +1,5 @@ +{ + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json b/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json new file mode 100644 index 0000000..7693fdf --- /dev/null +++ b/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json @@ -0,0 +1,10 @@ +{ + "address": { + "country": "USA", + "city": "Anytown", + "street": "Main Street", + "number": 111 + }, + "seller_name": "John Smith", + "property_id": "usa/anytown/main-street/111" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json new file mode 100644 index 0000000..154d225 --- /dev/null +++ b/unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json @@ -0,0 +1,6 @@ +{ + "property_id": "usa/anytown/main-street/123", + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json b/unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json new file mode 100644 index 0000000..ea61b51 --- /dev/null +++ b/unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json @@ -0,0 +1,3 @@ +{ + "property_id": "usa/anytown/main-street/123" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json new file mode 100644 index 0000000..1f352a3 --- /dev/null +++ b/unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json @@ -0,0 +1,5 @@ +{ + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json b/unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json new file mode 100644 index 0000000..5793374 --- /dev/null +++ b/unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json @@ -0,0 +1,3 @@ +{ + "property_id": "usa/some_other_town/street/878828" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/test_create_contract_apigw.py b/unicorn_contracts/tests/integration/test_create_contract_apigw.py index b754058..a87c920 100644 --- a/unicorn_contracts/tests/integration/test_create_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_create_contract_apigw.py @@ -1,60 +1,32 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os from unittest import TestCase - -import boto3 import requests -""" -Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test. -""" +from . import get_stack_output -class TestApiGateway(TestCase): +class TestCreateContract(TestCase): api_endpoint: str - @classmethod - def get_stack_name(cls) -> str: - stack_name = os.environ.get("AWS_SAM_STACK_NAME") - if not stack_name: - raise Exception( - "Cannot find env var AWS_SAM_STACK_NAME. \n" - "Please setup this environment variable with the stack name where we are running integration tests." - ) - - return stack_name - def setUp(self) -> None: - """ - Based on the provided env variable AWS_SAM_STACK_NAME, - here we use cloudformation API to find out what the HelloWorldApi URL is - """ - stack_name = TestApiGateway.get_stack_name() - - client = boto3.client("cloudformation") - - try: - response = client.describe_stacks(StackName=stack_name) - except Exception as e: - raise Exception( - f"Cannot find stack {stack_name}. \n" f'Please make sure stack with the name "{stack_name}" exists.' - ) from e - - stacks = response["Stacks"] + self.api_endpoint = get_stack_output('ApiUrl') - stack_outputs = stacks[0]["Outputs"] - api_outputs = [output for output in stack_outputs if output["OutputKey"] == "ApiUrl"] - print(api_outputs) - self.assertTrue(api_outputs, f"Cannot find output ApiUrl in stack {stack_name}") - self.api_endpoint = api_outputs[0]["OutputValue"] - - def test_create_contract(self): + def create_contract_invalid_payload_1(self): """ Call the API Gateway endpoint and check the response """ + payload = { + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" + } + response = requests.post(f'{self.api_endpoint}contracts', json = payload) + self.assertEqual(response.status_code, 400) + + def test_create_contract_valid_payload_1(self): payload = { "address": { "country": "USA", @@ -65,20 +37,7 @@ def test_create_contract(self): "seller_name": "John Smith", "property_id": "usa/anytown/main-street/111" } - response = requests.post(f'{self.api_endpoint}contracts', json = payload) + + response = requests.put(f'{self.api_endpoint}contract', json=payload) self.assertEqual(response.status_code, 200) - # https://stackoverflow.com/questions/20050913/python-unittests-assertdictcontainssubset-recommended-alternative - # self.assertDictEqual(response.json(), response.json() | {"message": "New contract has been successfully uploaded"}) - - def test_create_contract_wrong_payload(self): - """ - Call the API Gateway endpoint and check the response - """ - - payload = { - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" - } - response = requests.post(f'{self.api_endpoint}contracts', json = payload) - self.assertEqual(response.status_code, 400) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) diff --git a/unicorn_contracts/tests/integration/test_update_contract_apigw.py b/unicorn_contracts/tests/integration/test_update_contract_apigw.py index a610b4c..72af113 100644 --- a/unicorn_contracts/tests/integration/test_update_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_update_contract_apigw.py @@ -1,87 +1,57 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os from unittest import TestCase - -import boto3 import requests -""" -Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test. -""" +from . import get_stack_output -class TestApiGateway(TestCase): +class TestUpdateContract(TestCase): api_endpoint: str - @classmethod - def get_stack_name(cls) -> str: - stack_name = os.environ.get("AWS_SAM_STACK_NAME") - if not stack_name: - raise Exception( - "Cannot find env var AWS_SAM_STACK_NAME. \n" - "Please setup this environment variable with the stack name where we are running integration tests." - ) - - return stack_name - def setUp(self) -> None: - """ - Based on the provided env variable AWS_SAM_STACK_NAME, - here we use cloudformation API to find out what the HelloWorldApi URL is - """ - stack_name = TestApiGateway.get_stack_name() - - client = boto3.client("cloudformation") - - try: - response = client.describe_stacks(StackName=stack_name) - except Exception as e: - raise Exception( - f"Cannot find stack {stack_name}. \n" - f'Please make sure stack with the name "{stack_name}" exists.' - ) from e + self.api_endpoint = get_stack_output('ApiUrl') - stacks = response["Stacks"] - stack_outputs = stacks[0]["Outputs"] - api_outputs = [ - output for output in stack_outputs if output["OutputKey"] == "ApiUrl" - ] - print(api_outputs) - self.assertTrue(api_outputs, f"Cannot find output ApiUrl in stack {stack_name}") - - self.api_endpoint = api_outputs[0]["OutputValue"] + # NOTE: This test is not working as it supposed to. + # Need a way for OpenApi Spec to validate extra keys on payload + def test_update_existing_contract_invalid_payload_1(self): + payload = { + "property_id": "usa/anytown/main-street/123", + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" + } - def test_update_contract(self): - """ - Call the API Gateway endpoint and check the response - """ + response = requests.put(f'{self.api_endpoint}contract', json=payload) + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) - payload = { + def test_update_existing_contract_valid_payload(self): + payload = payload = { "property_id": "usa/anytown/main-street/123" } - response = requests.put(f"{self.api_endpoint}contracts", json=payload) - # self.assertDictEqual(response.json(), {"message": "hello world"}) - self.assertEqual(response.status_code, 200) - # https://stackoverflow.com/questions/20050913/python-unittests-assertdictcontainssubset-recommended-alternative - # self.assertDictEqual( - # response.json(), - # response.json() | {"message": "Contract has been successfully modified"}, - # ) - def test_update_contract_wrong_payload(self): - """ - Call the API Gateway endpoint and check the response - """ + response = requests.put(f'{self.api_endpoint}contract', json=payload) + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) + def test_update_missing_contract_invalid_payload_1(self): payload = { "add": "St.1 , Building 10", "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" } - response = requests.put(f"{self.api_endpoint}contracts", json=payload) + + response = requests.put(f'{self.api_endpoint}contract', json=payload) self.assertEqual(response.status_code, 400) - # self.assertDictEqual( - # response.json(), response.json() | {"message": "Unable to parse event body"} - # ) + self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) + + def test_update_missing_contract_valid_payload(self): + payload = { + "property_id": "usa/some_other_town/street/878828" + } + + response = requests.put(f'{self.api_endpoint}contract', json=payload) + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) From 9178454c064d390ff8daf33d866d35b390140bf6 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 24 Aug 2023 17:23:23 +1000 Subject: [PATCH 05/87] chore: Updated unit tests, et al --- .../contract_event_handler.py | 79 ++++++- .../create_contract_invalid_payload_1.json | 0 .../create_contract_valid_payload_1.json | 0 ...e_existing_contract_invalid_payload_1.json | 0 ...pdate_existing_contract_valid_payload.json | 0 ...te_missing_contract_invalid_payload_1.json | 0 ...update_missing_contract_valid_payload.json | 0 unicorn_contracts/tests/unit/conftest.py | 47 +++- .../create_contract_invalid_1.json | 5 + .../create_contract_valid_1.json | 10 + .../update_contract_valid_1.json | 3 + unicorn_contracts/tests/unit/events.py | 190 ++++++++++++++++ .../events/create_empty_dict_body_event.json | 85 ------- .../events/create_missing_body_event.json | 85 ------- .../tests/unit/events/create_valid_event.json | 85 ------- .../tests/unit/events/create_wrong_event.json | 85 ------- .../events/update_empty_dict_body_event.json | 85 ------- .../events/update_missing_body_event.json | 85 ------- .../tests/unit/events/update_valid_event.json | 85 ------- .../tests/unit/events/update_wrong_event.json | 85 ------- unicorn_contracts/tests/unit/helper.py | 18 +- .../tests/unit/lambda_context.py | 17 -- .../tests/unit/test_contract_event_handler.py | 123 +++++++++++ .../unit/test_create_contract_function.py | 177 ++++++++------- unicorn_contracts/tests/unit/test_helper.py | 86 -------- .../unit/test_update_contract_function.py | 208 +++++++++--------- 26 files changed, 640 insertions(+), 1003 deletions(-) rename unicorn_contracts/tests/integration/{events => event_payloads}/create_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{events => event_payloads}/create_contract_valid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{events => event_payloads}/update_existing_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{events => event_payloads}/update_existing_contract_valid_payload.json (100%) rename unicorn_contracts/tests/integration/{events => event_payloads}/update_missing_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{events => event_payloads}/update_missing_contract_valid_payload.json (100%) create mode 100644 unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json create mode 100644 unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json create mode 100644 unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json create mode 100644 unicorn_contracts/tests/unit/events.py delete mode 100644 unicorn_contracts/tests/unit/events/create_empty_dict_body_event.json delete mode 100644 unicorn_contracts/tests/unit/events/create_missing_body_event.json delete mode 100644 unicorn_contracts/tests/unit/events/create_valid_event.json delete mode 100644 unicorn_contracts/tests/unit/events/create_wrong_event.json delete mode 100644 unicorn_contracts/tests/unit/events/update_empty_dict_body_event.json delete mode 100644 unicorn_contracts/tests/unit/events/update_missing_body_event.json delete mode 100644 unicorn_contracts/tests/unit/events/update_valid_event.json delete mode 100644 unicorn_contracts/tests/unit/events/update_wrong_event.json delete mode 100644 unicorn_contracts/tests/unit/lambda_context.py create mode 100644 unicorn_contracts/tests/unit/test_contract_event_handler.py delete mode 100644 unicorn_contracts/tests/unit/test_helper.py diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 78b8c7d..554b116 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -2,15 +2,20 @@ # SPDX-License-Identifier: MIT-0 import os +import uuid +from datetime import datetime import boto3 +from boto3.dynamodb.conditions import Attr, Not +from botocore.exceptions import ClientError + from aws_lambda_powertools.logging import Logger from aws_lambda_powertools.metrics import Metrics from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.utilities.data_classes import event_source, SQSEvent from aws_lambda_powertools.utilities.typing import LambdaContext -# from contracts_service.enums import ContractStatus +from contracts_service.enums import ContractStatus # Initialise Environment variables @@ -46,7 +51,7 @@ def lambda_handler(event: SQSEvent, context: LambdaContext): @tracer.capture_method -def create_contract(contract: dict) -> None: +def create_contract(event: dict) -> None: """Create contract inside DynamoDB table Parameters @@ -58,8 +63,6 @@ def create_contract(contract: dict) -> None: dict DynamoDB put Item response """ - logger.info(contract) - # if contract id exists: # if constract status is APPROVED | DRAFT: # log message @@ -67,7 +70,41 @@ def create_contract(contract: dict) -> None: # create with status = DRAFT # return - # return table.put_item(Item=contract,) + logger.info(msg={"Creating contract": event}) + current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + contract = { + "property_id": event["property_id"], # PK + "address": event["address"], + "seller_name": event["seller_name"], + "contact_created": current_date, + "contract_last_modified_on": current_date, + "contract_id": str(uuid.uuid4()), + "contract_status": ContractStatus.DRAFT.name, + } + + try: + response = table.put_item( + Item=contract, + ConditionExpression= + Attr('property_id').not_exists() + | Attr('contract_status').is_in([ + ContractStatus.CANCELLED.name, + ContractStatus.CLOSED.name, + ContractStatus.EXPIRED.name, + ])) + + logger.info('var:response', response) + + # Annotate trace with contract status + tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) + + except ClientError as e: + match e.response["Error"]["Code"]: + case 'ConditionalCheckFailedException': + logger.exception(f"Unable to update contract Id {contract['property_id']}. Status is not in status DRAFT") + + raise e @tracer.capture_method @@ -83,8 +120,6 @@ def update_contract(contract: dict) -> None: dict DynamoDB put Item response """ - logger.info(contract) - # if contract doesnt exist # lod message # return @@ -93,4 +128,32 @@ def update_contract(contract: dict) -> None: # return # update contract status to APPROVED - # return table.put_item(Item=contract,) + logger.info(msg={"Updating contract": contract}) + + try: + contract["contract_status"] = ContractStatus.APPROVED.name + current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + response = table.update_item( + Key={ + 'property_id': contract['property_id'], + }, + UpdateExpression="set contract_status=:t, modified_date=:m", + ConditionExpression=Attr('contract_status').eq(ContractStatus.DRAFT.name), + ExpressionAttributeValues={ + ':t': contract['contract_status'], + ':m': current_date, + }, + ReturnValues="UPDATED_NEW") + logger.info('var:response', response) + + # Annotate trace with contract status + tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) + + except ClientError as e: + match e.response["Error"]["Code"]: + case 'ConditionalCheckFailedException': + logger.exception(f"Unable to update contract Id {contract['property_id']}. Status is not in status DRAFT") + case 'ResourceNotFoundException': + logger.exception(f"Unable to update contract Id {contract['property_id']}. Not Found") + raise e diff --git a/unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/event_payloads/create_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/event_payloads/create_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json b/unicorn_contracts/tests/integration/event_payloads/create_contract_valid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json rename to unicorn_contracts/tests/integration/event_payloads/create_contract_valid_payload_1.json diff --git a/unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/event_payloads/update_existing_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/event_payloads/update_existing_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json b/unicorn_contracts/tests/integration/event_payloads/update_existing_contract_valid_payload.json similarity index 100% rename from unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload.json rename to unicorn_contracts/tests/integration/event_payloads/update_existing_contract_valid_payload.json diff --git a/unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/event_payloads/update_missing_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/event_payloads/update_missing_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json b/unicorn_contracts/tests/integration/event_payloads/update_missing_contract_valid_payload.json similarity index 100% rename from unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload.json rename to unicorn_contracts/tests/integration/event_payloads/update_missing_contract_valid_payload.json diff --git a/unicorn_contracts/tests/unit/conftest.py b/unicorn_contracts/tests/unit/conftest.py index fb7b192..7e098d1 100644 --- a/unicorn_contracts/tests/unit/conftest.py +++ b/unicorn_contracts/tests/unit/conftest.py @@ -4,9 +4,10 @@ import os import boto3 +from aws_lambda_powertools.utilities.typing import LambdaContext import pytest -from moto import mock_dynamodb, mock_events +from moto import mock_dynamodb, mock_events, mock_sqs @pytest.fixture(scope='function') @@ -17,23 +18,47 @@ def aws_credentials(): os.environ['AWS_SECURITY_TOKEN'] = 'testing' os.environ['AWS_SESSION_TOKEN'] = 'testing' -# @pytest.fixture(scope='function') -# def env_vars(): -# os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' -# os.environ['SERVICE_NAMESPACE']='unicorn.contracts' -# os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' -# os.environ['POWERTOOLS_TRACE_DISABLED']='true' -# os.environ['POWERTOOLS_LOGGER_LOG_EVENT']='Info' -# os.environ['POWERTOOLS_LOGGER_SAMPLE_RATE']='0.1' -# os.environ['POWERTOOLS_METRICS_NAMESPACE']='unicorn.contracts' -# os.environ['LOG_LEVEL']='INFO' + +@pytest.fixture(scope='function') +def env_vars(): + os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' + os.environ['SERVICE_NAMESPACE']='unicorn.contracts' + os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' + os.environ['POWERTOOLS_TRACE_DISABLED']='true' + os.environ['POWERTOOLS_LOGGER_LOG_EVENT']='Info' + os.environ['POWERTOOLS_LOGGER_SAMPLE_RATE']='0.1' + os.environ['POWERTOOLS_METRICS_NAMESPACE']='unicorn.contracts' + os.environ['LOG_LEVEL']='INFO' + @pytest.fixture(scope='function') def dynamodb(aws_credentials): with mock_dynamodb(): yield boto3.resource('dynamodb', region_name='ap-southeast-2') + @pytest.fixture(scope='function') def eventbridge(aws_credentials): with mock_events(): yield boto3.client('events', region_name='ap-southeast-2') + + +@pytest.fixture(scope='function') +def sqs(aws_credentials): + with mock_sqs(): + yield boto3.client('sqs', region_name='ap-southeast-2') + + +@pytest.fixture(scope='function') +def lambda_context(): + context: LambdaContext = LambdaContext() + context._function_name="contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._function_version="$LATEST" + context._invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._memory_limit_in_mb=128 + context._aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" + context._log_group_name="/aws/lambda/contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" + # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) + # context._client_context=None + return context \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json b/unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json new file mode 100644 index 0000000..1177ce8 --- /dev/null +++ b/unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json @@ -0,0 +1,5 @@ +{ + "add": "St.1 , Building 10", + "seller": "John Smith", + "property": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} diff --git a/unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json b/unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json new file mode 100644 index 0000000..2b6245b --- /dev/null +++ b/unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json @@ -0,0 +1,10 @@ +{ + "address": { + "country": "USA", + "city": "Anytown", + "street": "Main Street", + "number": 123 + }, + "seller_name": "John Smith", + "property_id": "usa/anytown/main-street/123" +} diff --git a/unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json b/unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json new file mode 100644 index 0000000..b960967 --- /dev/null +++ b/unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json @@ -0,0 +1,3 @@ +{ + "property_id": "usa/anytown/main-street/123" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events.py b/unicorn_contracts/tests/unit/events.py new file mode 100644 index 0000000..25df2b2 --- /dev/null +++ b/unicorn_contracts/tests/unit/events.py @@ -0,0 +1,190 @@ +from typing import Any, List + +import json +import hashlib +import uuid +import base64 +import random +import time + +from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent, SQSEvent + + +def apigw_event(http_method: str, + resource: str, + body: Any, + b64encode: bool = False, + path: str = '', + stage: str = 'Local' + ) -> APIGatewayProxyEvent: + body_str = json.dumps(body) + if b64encode: + body_str = base64.b64encode(body_str.encode('utf-8')) + + return APIGatewayProxyEvent({ + "body": body_str, + "resource": resource, + "path": f'/{path}', + "httpMethod": http_method, + "isBase64Encoded": b64encode, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": f"/{path}" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ], + "Accept-Encoding": [ "gzip, deflate, sdch" ], + "Accept-Language": [ "en-US,en;q=0.8" ], + "Cache-Control": [ "max-age=0" ], + "CloudFront-Forwarded-Proto": [ "https" ], + "CloudFront-Is-Desktop-Viewer": [ "true" ], + "CloudFront-Is-Mobile-Viewer": [ "false" ], + "CloudFront-Is-SmartTV-Viewer": [ "false" ], + "CloudFront-Is-Tablet-Viewer": [ "false" ], + "CloudFront-Viewer-Country": [ "US" ], + "Host": [ "0123456789.execute-api.us-east-1.amazonaws.com" ], + "Upgrade-Insecure-Requests": [ "1" ], + "User-Agent": [ "Custom User Agent String" ], + "Via": [ "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" ], + "X-Amz-Cf-Id": [ "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" ], + "X-Forwarded-For": [ "127.0.0.1, 127.0.0.2" ], + "X-Forwarded-Port": [ "443" ], + "X-Forwarded-Proto": [ "https" ], + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": stage, + "requestId": str(uuid.uuid4()), + "requestTime": time.strftime("%d/%b/%Y:%H:%M:%S %z", time.gmtime()), + "requestTimeEpoch": int(time.time()), + "identity": { + "cognitoIdentityPoolId": None, + "accountId": None, + "cognitoIdentityId": None, + "caller": None, + "accessKey": None, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": None, + "userAgent": "Custom User Agent String", + "user": None, + }, + "path": f"/{stage}/{path}", + "resourcePath": resource, + "httpMethod": http_method, + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + }) + + +def sqs_event(messages: List[dict], + queue_name: str = 'MyQueue', + account_id: int = random.randint(100000000000,999999999999), + aws_region: str = 'us-east-1' + ) -> SQSEvent: + + records = [] + for message in messages: + body = json.dumps(message.get('body', '')) + md5ofbody = hashlib.md5(body.encode('utf-8')).hexdigest() + rcv_timestamp = int(time.time() + (random.randint(0, 500)/1000)) # Random delay of 0-500ms + + records.append({ + "messageId": str(uuid.uuid4()), + "receiptHandle": "MessageReceiptHandle", + "body": body, + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": f"{int(time.time())}", + "SenderId": f"{account_id}", + "ApproximateFirstReceiveTimestamp": str(rcv_timestamp), + }, + "messageAttributes": message.get('attributes', dict()), + "md5OfBody": md5ofbody, + "eventSource": "aws:sqs", + "eventSourceARN": f"arn:aws:sqs:{aws_region}:{account_id}:{queue_name}", + "awsRegion": aws_region, + }) + + return SQSEvent({ "Records": records }) + + + +# def sqs_event(messages: List[dict], +# queue_name: str = 'MyQueue', +# account_id: int = 0, +# aws_region: str = 'us-east-1' +# ) -> dict: +# try: +# return { +# "Records": [ +# sqs_record(m.get('body'), m.get('attributes', dict()), +# queue_name=queue_name, +# account_id=account_id, +# aws_region=aws_region) +# for m in messages +# ] +# } +# except KeyError as e: +# raise Exception(f'Each message should contain at least a `body` key: {messages}') from e + + +# def sqs_record(body: Any, +# attributes: dict[str,Any] = dict(), +# queue_name: str = 'MyQueue', +# account_id: int = 0, +# aws_region: str = 'us-east-1' +# ) -> dict: +# body = json.dumps(body) +# if account_id == 0: +# account_id = random.randint(100000000000,999999999999) + +# return { +# "messageId": str(uuid.uuid4()), +# "receiptHandle": "MessageReceiptHandle", +# "body": body, +# "attributes": { +# "ApproximateReceiveCount": "1", +# "SentTimestamp": int(time.time()), +# "SenderId": f"{account_id}", +# "ApproximateFirstReceiveTimestamp": str(int(time.time() + (random.randint(0, 500)/1000))), # Random delay of 0-500ms +# }, +# "messageAttributes": attributes, +# "md5OfBody": hashlib.md5(body).hexdigest(), +# "eventSource": "aws:sqs", +# "eventSourceARN": f"arn:aws:sqs:{aws_region}:{account_id}:{queue_name}", +# "awsRegion": aws_region, +# } diff --git a/unicorn_contracts/tests/unit/events/create_empty_dict_body_event.json b/unicorn_contracts/tests/unit/events/create_empty_dict_body_event.json deleted file mode 100644 index 02962d7..0000000 --- a/unicorn_contracts/tests/unit/events/create_empty_dict_body_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "POST", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/create_missing_body_event.json b/unicorn_contracts/tests/unit/events/create_missing_body_event.json deleted file mode 100644 index 19cc001..0000000 --- a/unicorn_contracts/tests/unit/events/create_missing_body_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "POST", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{'hello':'world'}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/create_valid_event.json b/unicorn_contracts/tests/unit/events/create_valid_event.json deleted file mode 100644 index 1b1f9cd..0000000 --- a/unicorn_contracts/tests/unit/events/create_valid_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "POST", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/create_wrong_event.json b/unicorn_contracts/tests/unit/events/create_wrong_event.json deleted file mode 100644 index 468bba8..0000000 --- a/unicorn_contracts/tests/unit/events/create_wrong_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "POST", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{\n \"add\": \"St.1 , Building 10\",\n \"seller\": \"John Smith\",\n \"property\": \"4781231c-bc30-4f30-8b30-7145f4dd1adb\"\n}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/update_empty_dict_body_event.json b/unicorn_contracts/tests/unit/events/update_empty_dict_body_event.json deleted file mode 100644 index 8f0145c..0000000 --- a/unicorn_contracts/tests/unit/events/update_empty_dict_body_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "PUT", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "PUT", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/update_missing_body_event.json b/unicorn_contracts/tests/unit/events/update_missing_body_event.json deleted file mode 100644 index 25c9cb4..0000000 --- a/unicorn_contracts/tests/unit/events/update_missing_body_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "PUT", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "PUT", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/update_valid_event.json b/unicorn_contracts/tests/unit/events/update_valid_event.json deleted file mode 100644 index 6981981..0000000 --- a/unicorn_contracts/tests/unit/events/update_valid_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "PUT", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "PUT", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{\n \"property_id\": \"usa/anytown/main-street/123\"}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/events/update_wrong_event.json b/unicorn_contracts/tests/unit/events/update_wrong_event.json deleted file mode 100644 index db43c9f..0000000 --- a/unicorn_contracts/tests/unit/events/update_wrong_event.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "resource": "/contracts", - "path": "/contracts", - "httpMethod": "PUT", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json", - "Host": "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com", - "Postman-Token": "a9133765-9386-46c5-a78b-c6b56f89f507", - "User-Agent": "PostmanRuntime/7.29.0", - "X-Amzn-Trace-Id": "Root=1-62cfb741-3fa699ec12c65b080082cd24", - "X-Forwarded-For": "54.240.193.3", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "k1zeivuzia.execute-api.ap-southeast-2.amazonaws.com" - ], - "Postman-Token": [ - "a9133765-9386-46c5-a78b-c6b56f89f507" - ], - "User-Agent": [ - "PostmanRuntime/7.29.0" - ], - "X-Amzn-Trace-Id": [ - "Root=1-62cfb741-3fa699ec12c65b080082cd24" - ], - "X-Forwarded-For": [ - "54.240.193.3" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": "null", - "multiValueQueryStringParameters": "null", - "pathParameters": "null", - "stageVariables": "null", - "requestContext": { - "resourceId": "ef5spq", - "resourcePath": "/contracts", - "httpMethod": "PUT", - "extendedRequestId": "VPmSUFCBywMFTXA=", - "requestTime": "14/Jul/2022:06:27:13 +0000", - "path": "/contracts/contracts", - "accountId": "xxxxxxxx", - "protocol": "HTTP/1.1", - "stage": "contracts", - "domainPrefix": "k1zeivuzia", - "requestTimeEpoch": 1657780033807, - "requestId": "4cc215ed-b6b7-41cc-89c2-4df49142848c", - "identity": { - "cognitoIdentityPoolId": "null", - "accountId": "null", - "cognitoIdentityId": "null", - "caller": "null", - "sourceIp": "54.240.193.3", - "principalOrgId": "null", - "accessKey": "null", - "cognitoAuthenticationType": "null", - "cognitoAuthenticationProvider": "null", - "userArn": "null", - "userAgent": "PostmanRuntime/7.29.0", - "user": "null" - }, - "domainName": "xxxxx.execute-api.ap-southeast-2.amazonaws.com", - "apiId": "k1zeivuzia" - }, - "body": "{\n \"add\": \"St.1 , Building 10\",\n \"sell\": \"John Smith\",\n \"prop\": \"4781231c-bc30-4f30-8b30-7145f4dd1adb\",\n \"cont\": \"8155fdc5-ba1d-4e51-bcbf-7b417c01a4f3\"}", - "isBase64Encoded": "False" -} \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/helper.py b/unicorn_contracts/tests/unit/helper.py index c4b08ff..eb10e8f 100644 --- a/unicorn_contracts/tests/unit/helper.py +++ b/unicorn_contracts/tests/unit/helper.py @@ -1,20 +1,17 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - -import os -import inspect import json +from pathlib import Path + TABLE_NAME = 'table1' EVENTBUS_NAME = 'test-eventbridge' +SQS_QUEUE_NAME = 'test_sqs' +EVENTS_DIR = Path(__file__).parent / 'event_payloads' def load_event(filename): - file_dir = os.path.dirname(os.path.abspath((inspect.stack()[0])[1])) - print(file_dir) - - with open(os.path.join(file_dir, filename), 'r') as f: - return json.load(f) + return json.load(open(EVENTS_DIR / f'{filename}.json', 'r')) def return_env_vars_dict(k=None): @@ -105,3 +102,8 @@ def create_ddb_table_contracts_with_entry(dynamodb): def create_test_eventbridge_bus(eventbridge): bus = eventbridge.create_event_bus(Name=EVENTBUS_NAME) return bus + + +def create_test_sqs_ingestion_queue(sqs): + queue = sqs.create_queue(QueueName=SQS_QUEUE_NAME) + return queue diff --git a/unicorn_contracts/tests/unit/lambda_context.py b/unicorn_contracts/tests/unit/lambda_context.py deleted file mode 100644 index c9a0f4c..0000000 --- a/unicorn_contracts/tests/unit/lambda_context.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -""" -Simple Lambda Context class to be passed to the lambda handler when test is invoked -""" - -class LambdaContext: - aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" - log_group_name="/aws/lambda/contractsService-CreateContractFunction-IWaQgsTEtLtX" - log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" - function_name="contractsService-CreateContractFunction-IWaQgsTEtLtX" - memory_limit_in_mb=128 - function_version="$LATEST" - invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-CreateContractFunction-IWaQgsTEtLtX" - client_context=None - #identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py new file mode 100644 index 0000000..3b179d0 --- /dev/null +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -0,0 +1,123 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import os +from importlib import reload + +import pytest +from unittest import mock, TestCase +from botocore.exceptions import ClientError + +from .events import sqs_event +from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_test_sqs_ingestion_queue +from .helper import TABLE_NAME + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_valid_create_event(dynamodb, sqs, lambda_context): + payload = load_event('create_contract_valid_1') + event = sqs_event([{'body': payload, 'attributes': {'HttpMethod': 'POST'}}]) + + # Loading function here so that mocking works correctly. + from contracts_service import contract_event_handler # noqa: F401 + # Reload is required to prevent function setup reuse from another test + reload(contract_event_handler) + + create_ddb_table_contracts(dynamodb) + create_test_sqs_ingestion_queue(sqs) + + contract_event_handler.lambda_handler(event, lambda_context) + + res = dynamodb.Table(TABLE_NAME).get_item(Key={'property_id': payload['property_id']}) + + TestCase().assertDictEqual(res, res | {"Item": payload}) + + + # Read value in DDB + # data = json.loads(ret["body"]) + + # assert ret["statusCode"] == 200 + # assert "property_id" in data.keys() + # assert "contract_status" in data.keys() + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_body_event(dynamodb, eventbridge, mocker, lambda_context): +# event = load_event('events/create_missing_body_event.json') + +# from contracts_service import contract_event_handler # noqa: F401 +# reload(contract_event_handler) + +# create_ddb_table_contracts(dynamodb) + +# contract_event_handler.lambda_handler(event, lambda_context) + +# # Read value in DDB +# # data = json.loads(ret["body"]) + +# # assert ret["statusCode"] == 200 +# # assert "property_id" in data.keys() +# # assert "contract_status" in data.keys() + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_empty_dict_body_event(dynamodb, eventbridge, mocker, lambda_context): +# apigw_event = load_event('events/create_empty_dict_body_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) + +# ret = create_contract_function.lambda_handler(apigw_event, lambda_context) +# data = json.loads(ret["body"]) + +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_wrong_event_data(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_wrong_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) + +# context = LambdaContext() + +# ret = create_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) + +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_missing_ddb_env_var(): + del os.environ['DYNAMODB_TABLE'] + # Loading function here so that mocking works correctly + with pytest.raises(EnvironmentError): + from contracts_service import contract_event_handler + reload(contract_event_handler) + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_missing_sm_env_var(): + del os.environ['SERVICE_NAMESPACE'] + + with pytest.raises(EnvironmentError): + from contracts_service import contract_event_handler + reload(contract_event_handler) + + +@mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) +def test_wrong_dynamodb_table(dynamodb, lambda_context): + event = sqs_event([{'body': load_event('create_contract_valid_1'), 'attributes': {'HttpMethod': 'POST'}}]) + + from contracts_service import contract_event_handler # noqa: F401 + + create_ddb_table_contracts(dynamodb) + + with pytest.raises(ClientError): + reload(contract_event_handler) + contract_event_handler.lambda_handler(event, lambda_context) diff --git a/unicorn_contracts/tests/unit/test_create_contract_function.py b/unicorn_contracts/tests/unit/test_create_contract_function.py index 77a7b2d..ae3552f 100644 --- a/unicorn_contracts/tests/unit/test_create_contract_function.py +++ b/unicorn_contracts/tests/unit/test_create_contract_function.py @@ -1,124 +1,123 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os -import json -from importlib import reload +# import os +# import json +# from importlib import reload -import pytest -from unittest import mock -from botocore.exceptions import ClientError +# import pytest +# from unittest import mock +# from botocore.exceptions import ClientError -from .lambda_context import LambdaContext -from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_test_eventbridge_bus +# from .helper import LambdaContext, load_event, return_env_vars_dict, create_ddb_table_contracts, create_test_eventbridge_bus -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_valid_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/create_valid_event.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_valid_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_valid_event.json') - # Loading function here so that mocking works correctly. - from contracts_service import create_contract_function +# # Loading function here so that mocking works correctly. +# from contracts_service import contract_event_handler # noqa: F401 - # Reload is required to prevent function setup reuse from another test - reload(create_contract_function) +# # Reload is required to prevent function setup reuse from another test +# reload(create_contract_function) - create_ddb_table_contracts(dynamodb) - create_test_eventbridge_bus(eventbridge) +# create_ddb_table_contracts(dynamodb) +# create_test_eventbridge_bus(eventbridge) - context = LambdaContext() - ret = create_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# context = LambdaContext() +# ret = create_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 200 - assert "property_id" in data.keys() - assert "contract_status" in data.keys() +# assert ret["statusCode"] == 200 +# assert "property_id" in data.keys() +# assert "contract_status" in data.keys() -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_body_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/create_missing_body_event.json') - from contracts_service import create_contract_function - reload(create_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_body_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_missing_body_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() - ret = create_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# context = LambdaContext() +# ret = create_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_empty_dict_body_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/create_empty_dict_body_event.json') - from contracts_service import create_contract_function - reload(create_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_empty_dict_body_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_empty_dict_body_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() +# context = LambdaContext() - ret = create_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# ret = create_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_wrong_event_data(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/create_wrong_event.json') - from contracts_service import create_contract_function - reload(create_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_wrong_event_data(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_wrong_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() +# context = LambdaContext() - ret = create_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# ret = create_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): - del os.environ['DYNAMODB_TABLE'] - # Loading function here so that mocking works correctly - from contracts_service import create_contract_function - with pytest.raises(EnvironmentError): - reload(create_contract_function) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): +# del os.environ['DYNAMODB_TABLE'] +# # Loading function here so that mocking works correctly +# from contracts_service import contract_event_handler # noqa: F401 +# with pytest.raises(EnvironmentError): +# reload(create_contract_function) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_eb_env_var(dynamodb, eventbridge, mocker): - del os.environ['EVENT_BUS'] - # Loading function here so that mocking works correctly - from contracts_service import helper - with pytest.raises(EnvironmentError): - reload(helper) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_eb_env_var(dynamodb, eventbridge, mocker): +# del os.environ['EVENT_BUS'] +# # Loading function here so that mocking works correctly +# from contracts_service import helper +# with pytest.raises(EnvironmentError): +# reload(helper) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_sm_env_var(dynamodb, eventbridge, mocker): - del os.environ['SERVICE_NAMESPACE'] - # Loading function here so that mocking works correctly - from contracts_service import helper - with pytest.raises(EnvironmentError): - reload(helper) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_sm_env_var(dynamodb, eventbridge, mocker): +# del os.environ['SERVICE_NAMESPACE'] +# # Loading function here so that mocking works correctly +# from contracts_service import helper +# with pytest.raises(EnvironmentError): +# reload(helper) -@mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) -def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/create_valid_event.json') - from contracts_service import create_contract_function - reload(create_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) +# def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/create_valid_event.json') +# from contracts_service import contract_event_handler # noqa: F401 +# reload(create_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() - with pytest.raises(ClientError): - create_contract_function.lambda_handler(apigw_event, context) +# context = LambdaContext() +# with pytest.raises(ClientError): +# create_contract_function.lambda_handler(apigw_event, context) diff --git a/unicorn_contracts/tests/unit/test_helper.py b/unicorn_contracts/tests/unit/test_helper.py deleted file mode 100644 index 05fe554..0000000 --- a/unicorn_contracts/tests/unit/test_helper.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -import os -import json -from importlib import reload - -import pytest -from unittest import mock -# from moto import mock_dynamodb, mock_events - -# from contracts_service.exceptions import EventValidationException - -from .helper import return_env_vars_dict, create_test_eventbridge_bus -from .lambda_context import LambdaContext - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_push_event(dynamodb, eventbridge, mocker): - context = LambdaContext() - - contract = { - "created_date": "current_date", - "contract_last_modified_on": "last_modified_date", - "address": "1", - "property_id": "1", - "contract_id": "1", - "contract_status": "New", - } - - # detail_type = "Contract created" - - from contracts_service import helper - reload(helper) - - create_test_eventbridge_bus(eventbridge) - - ret = helper.publish_event(contract, context.aws_request_id) - assert ret['FailedEntryCount'] == 0 - assert len(ret['Entries']) == 1 - for e in ret['Entries']: - assert "EventId" in e - assert "ErrorCode" not in e - assert "ErrorMessage" not in e - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_get_event_body(dynamodb, eventbridge, mocker): - - event = { - "body": "{\"add\": \"St.1 , Building 10\", \"sell\": \"John Smith\", \"prop\": \"4781231c-bc30-4f30-8b30-7145f4dd1adb\"}" - } - - from contracts_service import helper - reload(helper) - - ret = helper.get_event_body(event) - assert type(ret) == dict - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_get_event_body_bad_json(dynamodb, eventbridge, mocker): - - event = { - "body": "{\"add\": \"St.1 , Building 10\", \"sell\": \"John Smith\", \"prop\" \"4781231c-bc30-4f30-8b30-7145f4dd1adb\"}" - } - - from contracts_service import create_contract_function - reload(create_contract_function) - - with pytest.raises(json.decoder.JSONDecodeError): - create_contract_function.get_event_body(event) - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_get_event_body_bad_type(dynamodb, eventbridge, mocker): - - event = { - "body": 1 - } - - from contracts_service import create_contract_function - reload(create_contract_function) - - with pytest.raises(TypeError): - create_contract_function.get_event_body(event) diff --git a/unicorn_contracts/tests/unit/test_update_contract_function.py b/unicorn_contracts/tests/unit/test_update_contract_function.py index 410eae2..609d9ea 100644 --- a/unicorn_contracts/tests/unit/test_update_contract_function.py +++ b/unicorn_contracts/tests/unit/test_update_contract_function.py @@ -1,128 +1,128 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os -import json -from importlib import reload +# import os +# import json +# from importlib import reload -import pytest -from unittest import mock -# from moto import mock_dynamodb, mock_events -# from botocore.exceptions import ClientError +# import pytest +# from unittest import mock +# # from moto import mock_dynamodb, mock_events +# # from botocore.exceptions import ClientError -# from contracts_service.exceptions import EventValidationException +# # from contracts_service.exceptions import EventValidationException -from .lambda_context import LambdaContext -from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_ddb_table_contracts_with_entry, create_test_eventbridge_bus +# from .lambda_context import LambdaContext +# from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_ddb_table_contracts_with_entry, create_test_eventbridge_bus -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_valid_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/update_valid_event.json') - # Loading function here so that mocking works correctly - from contracts_service import update_contract_function - reload(update_contract_function) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_valid_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/update_valid_event.json') +# # Loading function here so that mocking works correctly +# from contracts_service import update_contract_function +# reload(update_contract_function) - create_ddb_table_contracts_with_entry(dynamodb) - create_test_eventbridge_bus(eventbridge) +# create_ddb_table_contracts_with_entry(dynamodb) +# create_test_eventbridge_bus(eventbridge) - context = LambdaContext() - ret = update_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# context = LambdaContext() +# ret = update_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 200 - assert "contract_status" in data.keys() - assert "property_id" in data.keys() +# assert ret["statusCode"] == 200 +# assert "contract_status" in data.keys() +# assert "property_id" in data.keys() -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_body_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/update_missing_body_event.json') - from contracts_service import update_contract_function - reload(update_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_body_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/update_missing_body_event.json') +# from contracts_service import update_contract_function +# reload(update_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() - ret = update_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# context = LambdaContext() +# ret = update_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_empty_dict_body_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/update_empty_dict_body_event.json') - from contracts_service import update_contract_function - reload(update_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_empty_dict_body_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/update_empty_dict_body_event.json') +# from contracts_service import update_contract_function +# reload(update_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() +# context = LambdaContext() - ret = update_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) +# ret = update_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_wrong_event_data(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/update_wrong_event.json') - from contracts_service import update_contract_function - reload(update_contract_function) - create_ddb_table_contracts(dynamodb) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_wrong_event_data(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/update_wrong_event.json') +# from contracts_service import update_contract_function +# reload(update_contract_function) +# create_ddb_table_contracts(dynamodb) - context = LambdaContext() +# context = LambdaContext() - ret = update_contract_function.lambda_handler(apigw_event, context) - data = json.loads(ret["body"]) - - assert ret["statusCode"] == 400 - assert "message" in ret["body"] - assert data["message"] == "Event body not valid." - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): - del os.environ['DYNAMODB_TABLE'] - load_event('events/update_valid_event.json') - # Loading function here so that mocking works correctly - from contracts_service import update_contract_function - with pytest.raises(EnvironmentError): - reload(update_contract_function) - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_eb_env_var(dynamodb, eventbridge, mocker): - del os.environ['EVENT_BUS'] - load_event('events/update_valid_event.json') - # Loading function here so that mocking works correctly - from contracts_service import helper - with pytest.raises(EnvironmentError): - reload(helper) - - -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_sm_env_var(dynamodb, eventbridge, mocker): - del os.environ['SERVICE_NAMESPACE'] - load_event('events/update_valid_event.json') - # Loading function here so that mocking works correctly - from contracts_service import helper - with pytest.raises(EnvironmentError): - reload(helper) - - -@mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) -def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/update_valid_event.json') - from contracts_service import update_contract_function - reload(update_contract_function) - create_ddb_table_contracts_with_entry(dynamodb) - - context = LambdaContext() - # with pytest.raises(ClientError): - ret = update_contract_function.lambda_handler(apigw_event, context) - assert ret["statusCode"] == 400 +# ret = update_contract_function.lambda_handler(apigw_event, context) +# data = json.loads(ret["body"]) + +# assert ret["statusCode"] == 400 +# assert "message" in ret["body"] +# assert data["message"] == "Event body not valid." + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): +# del os.environ['DYNAMODB_TABLE'] +# load_event('events/update_valid_event.json') +# # Loading function here so that mocking works correctly +# from contracts_service import update_contract_function +# with pytest.raises(EnvironmentError): +# reload(update_contract_function) + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_eb_env_var(dynamodb, eventbridge, mocker): +# del os.environ['EVENT_BUS'] +# load_event('events/update_valid_event.json') +# # Loading function here so that mocking works correctly +# from contracts_service import helper +# with pytest.raises(EnvironmentError): +# reload(helper) + + +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_missing_sm_env_var(dynamodb, eventbridge, mocker): +# del os.environ['SERVICE_NAMESPACE'] +# load_event('events/update_valid_event.json') +# # Loading function here so that mocking works correctly +# from contracts_service import helper +# with pytest.raises(EnvironmentError): +# reload(helper) + + +# @mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) +# def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/update_valid_event.json') +# from contracts_service import update_contract_function +# reload(update_contract_function) +# create_ddb_table_contracts_with_entry(dynamodb) + +# context = LambdaContext() +# # with pytest.raises(ClientError): +# ret = update_contract_function.lambda_handler(apigw_event, context) +# assert ret["statusCode"] == 400 From ed05ee8e83d7182201510e353e2a7d3b67d449bd Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 25 Aug 2023 15:26:42 +1000 Subject: [PATCH 06/87] feat: Add EB Pipes --- unicorn_contracts/Makefile | 98 +++- unicorn_contracts/api.yaml | 12 +- unicorn_contracts/samconfig.yaml | 2 +- .../contract_event_handler.py | 98 ++-- unicorn_contracts/template.yaml | 299 +++++++---- unicorn_contracts/template_steve.yaml | 474 ++++++++++++++++++ .../create_contract_invalid_payload_1.json | 0 .../create_contract_valid_payload_1.json | 4 +- ...e_existing_contract_invalid_payload_1.json | 0 ...te_existing_contract_valid_payload_1.json} | 0 ...te_missing_contract_invalid_payload_1.json | 0 ...ate_missing_contract_valid_payload_1.json} | 0 .../transformations/ddb_contract.jq | 14 + .../create_contract.json | 16 + .../update_contract.json | 16 + .../pipes/pipes_payloads/create_contract.json | 55 ++ .../pipes/pipes_payloads/update_contract.json | 94 ++++ .../streams_payloads/create_contract.json | 47 ++ .../streams_payloads/delete_contract.json | 50 ++ .../streams_payloads/update_contract.json | 86 ++++ 20 files changed, 1206 insertions(+), 159 deletions(-) create mode 100644 unicorn_contracts/template_steve.yaml rename unicorn_contracts/tests/integration/{event_payloads => events}/create_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{event_payloads => events}/create_contract_valid_payload_1.json (66%) rename unicorn_contracts/tests/integration/{event_payloads => events}/update_existing_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{event_payloads/update_existing_contract_valid_payload.json => events/update_existing_contract_valid_payload_1.json} (100%) rename unicorn_contracts/tests/integration/{event_payloads => events}/update_missing_contract_invalid_payload_1.json (100%) rename unicorn_contracts/tests/integration/{event_payloads/update_missing_contract_valid_payload.json => events/update_missing_contract_valid_payload_1.json} (100%) create mode 100644 unicorn_contracts/tests/integration/transformations/ddb_contract.jq create mode 100644 unicorn_contracts/tests/pipes/event_bridge_payloads/create_contract.json create mode 100644 unicorn_contracts/tests/pipes/event_bridge_payloads/update_contract.json create mode 100644 unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json create mode 100644 unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json create mode 100644 unicorn_contracts/tests/pipes/streams_payloads/create_contract.json create mode 100644 unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json create mode 100644 unicorn_contracts/tests/pipes/streams_payloads/update_contract.json diff --git a/unicorn_contracts/Makefile b/unicorn_contracts/Makefile index a942d09..0bd1698 100644 --- a/unicorn_contracts/Makefile +++ b/unicorn_contracts/Makefile @@ -1,9 +1,18 @@ -stackName := uni-prop-local-contract +#### Global Variables +stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.yaml) + +#### Test Variables +apiUrl = $(call cf_output,$(stackName),ApiUrl) +ddbPropertyId = $(call get_ddb_key,create_contract_valid_payload_1) + + +#### Build/Deploy Tasks ci: clean build deploy build: - # cfn-lint template.yaml -a cfn_lint_serverless.rules + # sam validate --lint + cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt sam build -c $(DOCKER_OPTS) @@ -13,9 +22,14 @@ deps: deploy: build sam deploy --no-confirm-changeset -sync: - sam sync --stack-name $(stackName) --watch +# NOTE: [2023-05-09] This is a fix for installing Poetry dependencies in GitHub Actions +ci_init: + poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt --with dev + poetry run pip install -r src/requirements.txt + poetry install -n + +#### Tests test: unit-test unit-test: @@ -24,6 +38,37 @@ unit-test: integration-test: deps poetry run pytest tests/integration/ +curl-test: clean-tests + $(call runif,CREATE CONTRACT) + $(call mcurl,POST,create_contract_valid_payload_1) + + $(call runif,Query DDB) + $(call ddb_get,$(ddbPropertyId)) + + $(call runif,UPDATE CONTRACT) + $(call mcurl,PUT,update_existing_contract_valid_payload_1) + + $(call runif,Query DDB) + $(call ddb_get,$(ddbPropertyId)) + + $(call runif,Delete DDB Items) + $(MAKE) clean-tests + + @echo "[DONE]" + +x: + @echo "apiUrl: $(apiUrl)" + @echo "ddbPropertyId: $(ddbPropertyId)" + @echo "$(call get_ddb_key,create_contract_valid_payload_1)" + +clean-tests: + $(call ddb_delete,$(ddbPropertyId)) || true + + +#### Utilities +sync: + sam sync --stack-name $(stackName) --watch + logs: sam logs --stack-name $(stackName) -t @@ -35,8 +80,43 @@ clean: delete: sam delete --stack-name $(stackName) --no-prompts -# NOTE: [2023-05-09] This is a fix for installing Poetry dependencies in GitHub Actions -ci_init: - poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt --with dev - poetry run pip install -r src/requirements.txt - poetry install -n + +#### Helper Functions +define runif + @echo + @echo "Run $(1) now?" + @read + @echo "Running $(1)" +endef + +define ddb_get + @aws dynamodb get-item \ + --table-name $(call cf_output,$(stackName),ContractsTableName) \ + --key '$(1)' \ + | jq -f tests/integration/transformations/ddb_contract.jq +endef + +define ddb_delete + aws dynamodb delete-item \ + --table-name $(call cf_output,$(stackName),ContractsTableName) \ + --key '$(1)' +endef + +define mcurl + curl -X $(1) -H "Content-type: application/json" -d @$(call payload,$(2)) $(apiUrl)contract +endef + +define get_ddb_key +$(shell jq '. | {property_id:{S:.property_id}}' $(call payload,$(1)) | tr -d ' ') +endef + +define payload +tests/integration/events/$(1).json +endef + +define cf_output + $(shell aws cloudformation describe-stacks \ + --output text \ + --stack-name $(1) \ + --query 'Stacks[0].Outputs[?OutputKey==`$(2)`].OutputValue') +endef diff --git a/unicorn_contracts/api.yaml b/unicorn_contracts/api.yaml index aee71d4..936bee7 100644 --- a/unicorn_contracts/api.yaml +++ b/unicorn_contracts/api.yaml @@ -22,15 +22,15 @@ paths: x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: - Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + Fn::GetAtt: [ ContractsApiIntegrationRole, Arn ] httpMethod: "POST" uri: - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${ContractServiceIngestQueue.QueueName}" + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" responses: default: statusCode: "200" responseTemplates: - application/json: "{\"message\":\"OK\"}" + application/json: '{"message":"OK"}' requestParameters: integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" requestTemplates: @@ -84,15 +84,15 @@ paths: x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: - Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + Fn::GetAtt: [ ContractsApiIntegrationRole, Arn ] httpMethod: "POST" uri: - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${ContractServiceIngestQueue.QueueName}" + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" responses: default: statusCode: "200" responseTemplates: - application/json: "{\"message\":\"OK\"}" + application/json: '{"message":"OK"}' requestParameters: integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" requestTemplates: diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index ae588c2..3ac9ade 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -12,7 +12,7 @@ default: parallel: true deploy: parameters: - disable_rollback: true + disable_rollback: false confirm_changeset: false fail_on_empty_changeset: false capabilities: "CAPABILITY_IAM" diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 554b116..674d368 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -6,7 +6,7 @@ from datetime import datetime import boto3 -from boto3.dynamodb.conditions import Attr, Not +from boto3.dynamodb.conditions import Attr from botocore.exceptions import ClientError from aws_lambda_powertools.logging import Logger @@ -41,19 +41,31 @@ def lambda_handler(event: SQSEvent, context: LambdaContext): # Multiple records can be delivered in a single event for record in event.records: - match record.message_attributes.get('HttpMethod'): - case 'POST': - create_contract(record.json_body) - case 'PUT': - update_contract(record.json_body) - case other: - raise Exception(f'Unable to handle HttpMethod {other}') + http_method = record.message_attributes.get('HttpMethod', {}).get('stringValue') + + if http_method == 'POST': + create_contract(record.json_body) + elif http_method == 'PUT': + update_contract(record.json_body) + else: + raise Exception(f'Unable to handle HttpMethod {http_method}') @tracer.capture_method def create_contract(event: dict) -> None: """Create contract inside DynamoDB table + Execution logic: + if contract does not exist + or contract status is either of [ CANCELLED | CLOSED | EXPIRED] + then + create or replace contract with status = DRAFT + log response + log trace info + return + else + log exception message + Parameters ---------- contract (dict): _description_ @@ -63,16 +75,8 @@ def create_contract(event: dict) -> None: dict DynamoDB put Item response """ - # if contract id exists: - # if constract status is APPROVED | DRAFT: - # log message - # return - # create with status = DRAFT - # return - - logger.info(msg={"Creating contract": event}) - current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") contract = { "property_id": event["property_id"], # PK "address": event["address"], @@ -83,34 +87,50 @@ def create_contract(event: dict) -> None: "contract_status": ContractStatus.DRAFT.name, } + logger.info(msg={"Creating contract": contract, "From event": event}) + try: response = table.put_item( Item=contract, ConditionExpression= - Attr('property_id').not_exists() + Attr('property_id').not_exists() | Attr('contract_status').is_in([ ContractStatus.CANCELLED.name, ContractStatus.CLOSED.name, ContractStatus.EXPIRED.name, ])) - - logger.info('var:response', response) + logger.info(f'var:response - "{response}"') # Annotate trace with contract status tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) except ClientError as e: - match e.response["Error"]["Code"]: - case 'ConditionalCheckFailedException': - logger.exception(f"Unable to update contract Id {contract['property_id']}. Status is not in status DRAFT") - - raise e + code = e.response["Error"]["Code"] + if code == 'ConditionalCheckFailedException': + logger.info(f""" + Unable to create contract for Property {contract['property_id']}. + There already is a contract for this property in status {ContractStatus.DRAFT.name} or {ContractStatus.APPROVED.name} + """) + else: + raise e @tracer.capture_method def update_contract(contract: dict) -> None: """Update an existing contract inside DynamoDB table + Execution logic: + + if contract exists exist + and contract status is either of [ DRAFT ] + then + update contract status to APPROVED + log response + log trace info + return + else + log exception message + Parameters ---------- contract (dict): _description_ @@ -120,13 +140,6 @@ def update_contract(contract: dict) -> None: dict DynamoDB put Item response """ - # if contract doesnt exist - # lod message - # return - # if contract status in [ APPROVED | CANCELLED | CLOSED | EXPIRED ] - # close - # return - # update contract status to APPROVED logger.info(msg={"Updating contract": contract}) @@ -139,21 +152,26 @@ def update_contract(contract: dict) -> None: 'property_id': contract['property_id'], }, UpdateExpression="set contract_status=:t, modified_date=:m", - ConditionExpression=Attr('contract_status').eq(ContractStatus.DRAFT.name), + ConditionExpression= + Attr('property_id').exists() + & Attr('contract_status').is_in([ + ContractStatus.DRAFT.name + ]), ExpressionAttributeValues={ ':t': contract['contract_status'], ':m': current_date, }, ReturnValues="UPDATED_NEW") - logger.info('var:response', response) + logger.info(f'var:response - "{response}"') # Annotate trace with contract status tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) except ClientError as e: - match e.response["Error"]["Code"]: - case 'ConditionalCheckFailedException': - logger.exception(f"Unable to update contract Id {contract['property_id']}. Status is not in status DRAFT") - case 'ResourceNotFoundException': - logger.exception(f"Unable to update contract Id {contract['property_id']}. Not Found") - raise e + code = e.response["Error"]["Code"] + if code == 'ConditionalCheckFailedException': + logger.exception(f"Unable to update contract Id {contract['property_id']}. Status is not in status DRAFT") + elif code == 'ResourceNotFoundException': + logger.exception(f"Unable to update contract Id {contract['property_id']}. Not Found") + else: + raise e diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 0412ede..12e8a31 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -15,8 +15,7 @@ Metadata: - I3042 - ES1007 - ES1001 - - ES6000 - - ES4000 + - WS2001 #### PARAMETERS Parameters: @@ -28,12 +27,6 @@ Parameters: - Dev - Prod - ContractsServiceNamespace: - Type: String - Default: "unicorn.contracts" - AllowedValues: - - "unicorn.contracts" - #### MAPPINGS Mappings: LogsRetentionPeriodMap: @@ -43,15 +36,20 @@ Mappings: Days: 3 Prod: Days: 14 + Constants: + ServiceNamespace: + Value: "unicorn.contracts" + ProjectName: + Value: "AWS Serverless Developer Experience" #### CONDITIONS Conditions: - IsProd: !Equals [ !Ref Stage, Prod ] + IsProd: !Equals [!Ref Stage, Prod] #### GLOBALS Globals: Api: - OpenApiVersion: "3.0.1" + OpenApiVersion: 3.0.1 Function: Runtime: python3.11 MemorySize: 128 @@ -62,38 +60,42 @@ Globals: Environment: Variables: DYNAMODB_TABLE: !Ref ContractsTable - SERVICE_NAMESPACE: !Ref ContractsServiceNamespace - POWERTOOLS_SERVICE_NAME: !Ref ContractsServiceNamespace - POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default - POWERTOOLS_LOGGER_LOG_EVENT: !If [ IsProd, "false", "true" ] # Logs incoming event, default - POWERTOOLS_LOGGER_SAMPLE_RATE: !If [ IsProd, "0.1", "0" ] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !Ref ContractsServiceNamespace # Metric Namespace - LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default + POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default + POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default + POWERTOOLS_METRICS_NAMESPACE: !FindInMap [ + Constants, + ServiceNamespace, + Value, + ] # Metric Namespace + LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage - project: AWS Serverless Developer Experience - service: !Ref ContractsServiceNamespace + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] #### RESOURCES Resources: #### SSM PARAMETERS # Services own and share their namespace - UnicornContractsNamespace: + UnicornContractsNamespaceParam: Type: AWS::SSM::Parameter Properties: Type: String Name: !Sub /UniProp/${Stage}/UnicornContractsNamespace - Value: !Ref ContractsServiceNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] # Services share their event bus name and arn - UnicornContractsEventBusName: + UnicornContractsEventBusNameParam: Type: AWS::SSM::Parameter Properties: Type: String Name: !Sub /UniProp/${Stage}/UnicornContractsEventBus Value: !GetAtt UnicornContractsEventBus.Name - UnicornContractsEventBusArn: + UnicornContractsEventBusArnParam: Type: AWS::SSM::Parameter Properties: Type: String @@ -115,7 +117,7 @@ Resources: IngestQueue: Type: SQS Properties: - Queue: !GetAtt ContractServiceIngestQueue.Arn + Queue: !GetAtt UnicornContractsIngestQueue.Arn BatchSize: 1 Enabled: true ScalingConfig: @@ -127,14 +129,12 @@ Resources: UpdateReplacePolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" - RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] - + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + #### API GATEWAY REST API - ContractsApi: + UnicornContractsApi: Type: AWS::Serverless::Api DependsOn: ContractsApiGwAccountConfig - UpdateReplacePolicy: Delete - DeletionPolicy: Delete Properties: StageName: !Ref Stage EndpointConfiguration: @@ -144,11 +144,11 @@ Resources: - MetricsEnabled: true ResourcePath: /* HttpMethod: "*" - LoggingLevel: !If [ IsProd, ERROR, INFO ] + LoggingLevel: !If [IsProd, ERROR, INFO] ThrottlingBurstLimit: 10 ThrottlingRateLimit: 100 AccessLogSetting: - DestinationArn: !GetAtt ContractsApiLogGroup.Arn + DestinationArn: !GetAtt UnicornContractsApiLogGroup.Arn Format: !ToJsonString requestId: $context.requestId integration-error: $context.integration.error @@ -158,28 +158,19 @@ Resources: integration-integrationStatus: $context.integration.integrationStatus response-latency: $context.responseLatency status: $context.status - DefinitionBody: - "Fn::Transform": - Name: "AWS::Include" - Parameters: - Location: 'api.yaml' + DefinitionBody: !Transform + Name: "AWS::Include" + Parameters: + Location: "api.yaml" Tags: stage: !Ref Stage - project: AWS Serverless Developer Experience - namespace: !Ref ContractsServiceNamespace + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] ContractsApiGwAccountConfig: Type: AWS::ApiGateway::Account Properties: - CloudWatchRoleArn: !GetAtt ContractsApiGwAccountConfigRole.Arn - - # API GW Cloudwatch Log Group - ContractsApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] + CloudWatchRoleArn: !GetAtt UnicornContractsApiGwAccountConfigRole.Arn # API GW IAM roles ContractsApiIntegrationRole: @@ -187,8 +178,8 @@ Resources: Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow + Action: sts:AssumeRole Principal: Service: apigateway.amazonaws.com Policies: @@ -197,11 +188,19 @@ Resources: Statement: - Effect: Allow Action: - - 'sqs:SendMessage' - - 'sqs:GetQueueUrl' - Resource: !GetAtt ContractServiceIngestQueue.Arn + - sqs:SendMessage + - sqs:GetQueueUrl + Resource: !GetAtt UnicornContractsIngestQueue.Arn + + # API GW Cloudwatch Log Group + UnicornContractsApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - ContractsApiGwAccountConfigRole: + UnicornContractsApiGwAccountConfigRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -210,46 +209,49 @@ Resources: Principal: Service: - apigateway.amazonaws.com - Action: 'sts:AssumeRole' + Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs #### INGEST QUEUES - ContractServiceIngestQueue: + UnicornContractsIngestQueue: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - QueueName: !Sub ContractServiceIngestQueue-${Stage} RedrivePolicy: - deadLetterTargetArn: !GetAtt ContractServiceIngestDLQ.Arn + deadLetterTargetArn: !GetAtt UnicornContractsIngestDLQ.Arn maxReceiveCount: 1 VisibilityTimeout: 20 Tags: - Key: stage Value: !Ref Stage - Key: project - Value: AWS Serverless Developer Experience + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !Ref ContractsServiceNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - ContractServiceIngestDLQ: + UnicornContractsIngestDLQ: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete DeletionPolicy: Delete + Metadata: + cfn-lint: + config: + ignore_checks: + - ES6000 Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - QueueName: !Sub ContractServiceIngestDLQ-${Stage} Tags: - Key: stage Value: !Ref Stage - Key: project - Value: AWS Serverless Developer Experience + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !Ref ContractsServiceNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] #### DYNAMODB TABLE ContractsTable: @@ -258,19 +260,19 @@ Resources: DeletionPolicy: Delete Properties: AttributeDefinitions: - - AttributeName: property_id # OR PropertyId? + - AttributeName: property_id AttributeType: S KeySchema: - - AttributeName: property_id # OR PropertyId? + - AttributeName: property_id KeyType: HASH StreamSpecification: StreamViewType: NEW_AND_OLD_IMAGES BillingMode: PAY_PER_REQUEST Tags: - Key: project - Value: AWS Serverless Developer Experience + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !Ref ContractsServiceNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage Value: !Ref Stage @@ -281,7 +283,7 @@ Resources: Properties: Name: !Sub UnicornContractsBus-${Stage} - # Event bus policy to restrict who can publish events (should only be services from ContractsServiceNamespace) + # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) ContractEventsBusPublishPolicy: Type: AWS::Events::EventBusPolicy Properties: @@ -292,25 +294,59 @@ Resources: Principal: AWS: - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - Action: events:PutEvents + Action: + - events:PutEvents Resource: !GetAtt UnicornContractsEventBus.Arn Condition: StringEquals: events:source: - - !Ref ContractsServiceNamespace + - !FindInMap [Constants, ServiceNamespace, Value] + + # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. + # Update this policy as you get new subscribers by adding their namespace to events:source + # PropertiesServiceRulePolicy: + # Type: "AWS::Events::EventBusPolicy" + # Properties: + # EventBusName: !Ref UnicornContractsEventBus + # StatementId: !Sub CreateRulePolicy-${Stage} + # Statement: + # Effect: Allow + # Principal: + # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + # Action: + # - events:PutRule + # - events:DeleteRule + # - events:DescribeRule + # - events:DisableRule + # - events:EnableRule + # - events:PutTargets + # - events:RemoveTargets + # Resource: + # - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornContractsEventBus.Name}/*" + # Condition: + # StringEqualsIfExists: + # "events:creatorAccount": "${aws:PrincipalAccount}" + # StringEquals: + # "events:source": + # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + # "Null": + # "events:source": "false" # Catchall rule used for development purposes. UnicornContractsCatchAllRule: Type: AWS::Events::Rule + Metadata: + cfn-lint: + config: + ignore_checks: + - ES4000 Properties: Name: contracts.catchall Description: Catch all events published by the contracts service. EventBusName: !Ref UnicornContractsEventBus EventPattern: - account: - - !Ref AWS::AccountId source: - - !Ref ContractsServiceNamespace + - prefix: "" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn @@ -322,51 +358,112 @@ Resources: UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - LogGroupName: !Sub /aws/events/${Stage}/${ContractsServiceNamespace}-catchall - RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Permissions to allow EventBridge to send logs to CloudWatch EventBridgeCloudWatchLogGroupPolicy: Type: AWS::Logs::ResourcePolicy Properties: PolicyName: !Sub EventBridgeToCWLogsPolicy-${Stage} # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument - PolicyDocument: !Sub - - | - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "delivery.logs.amazonaws.com", - "events.amazonaws.com" - ] - }, - "Action": [ - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": [ - "${contractsServiceLogGroup}" + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "delivery.logs.amazonaws.com", + "events.amazonaws.com" ] - } - ] - } - - contractsServiceLogGroup: UnicornContractsCatchAllLogGroup.Arn + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "${UnicornContractsCatchAllLogGroup.Arn}" + ] + } + ] + } + + #### EVENT BRIDGE PIPES + # Pipe changed on DynamoDB Table to UnicornContractsEventBus + DdbStreamToEventPipe: + Type: AWS::Pipes::Pipe + Properties: + RoleArn: !GetAtt DdbStreamToEventPipeRole.Arn + Source: !GetAtt ContractsTable.StreamArn + SourceParameters: + DynamoDBStreamParameters: + StartingPosition: LATEST + OnPartialBatchItemFailure: AUTOMATIC_BISECT + BatchSize: 1 + # DeadLetterConfig: + # Arn: String + FilterCriteria: + Filters: + - Pattern: !ToJsonString + eventName: [INSERT, MODIFY] + dynamodb: + NewImage: + contract_status: + S: [DRAFT, APPROVED] + # Enrichment: arn:aws:execute-api:us-east-1:123456789123:53eo2i89p9/*/POST/pets + Target: !GetAtt UnicornContractsEventBus.Arn + TargetParameters: + EventBridgeEventBusParameters: + Source: !FindInMap [Constants, ServiceNamespace, Value] + DetailType: ContractStatusChanged + InputTemplate: !ToJsonString + property_id: "<$.dynamodb.NewImage.property_id.S>" + contract_id: "<$.dynamodb.NewImage.contract_id.S>" + contract_status: "<$.dynamodb.NewImage.contract_status.S>" + contract_last_modified_on: "<$.dynamodb.NewImage.contract_last_modified_on.S>" + + # IAM Role for Event Bridge Pipe + DdbStreamToEventPipeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + Action: sts:AssumeRole + Effect: Allow + Principal: + Service: pipes.amazonaws.com + Policies: + - PolicyName: AllowDdbStreamRead + PolicyDocument: + Statement: + - Effect: Allow + Action: + - dynamodb:ListStreams + Resource: "*" + - Effect: Allow + Action: + - dynamodb:DescribeStream + - dynamodb:GetRecords + - dynamodb:GetShardIterator + Resource: !GetAtt ContractsTable.StreamArn + - Effect: Allow + Action: + - events:PutEvents + Resource: !GetAtt UnicornContractsEventBus.Arn #### OUTPUTS Outputs: BaseUrl: Description: Web service API endpoint - Value: !Sub "https://${ContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" + Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" ApiUrl: Description: Contract service API endpoint - Value: !Sub "https://${ContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" IngestQueueUrl: Description: URL for the Ingest SQS Queue - Value: !GetAtt ContractServiceIngestQueue.QueueUrl + Value: !GetAtt UnicornContractsIngestQueue.QueueUrl ContractsTableName: Description: DynamoDB table storing contract information @@ -378,4 +475,4 @@ Outputs: IsProd: Description: Is Production? - Value: !If [ IsProd, 'true', 'false' ] + Value: !If [IsProd, "true", "false"] diff --git a/unicorn_contracts/template_steve.yaml b/unicorn_contracts/template_steve.yaml new file mode 100644 index 0000000..3962897 --- /dev/null +++ b/unicorn_contracts/template_steve.yaml @@ -0,0 +1,474 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: 2010-09-09 +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 +Description: > + Unicorn Contracts Service resources. + +#### METADATA +Metadata: + cfn-lint: + config: + ignore_checks: + - I3042 + - ES1007 + - ES1001 + - WS2001 + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### MAPPINGS +Mappings: + LogsRetentionPeriodMap: + Local: + Days: 3 + Dev: + Days: 3 + Prod: + Days: 14 + Constants: + ServiceNamespace: + Value: "unicorn.contracts" + ProjectName: + Value: "AWS Serverless Developer Experience" + +#### CONDITIONS +Conditions: + IsProd: !Equals [!Ref Stage, Prod] + +#### GLOBALS +Globals: + Api: + OpenApiVersion: 3.0.1 + Function: + Runtime: dotnet6 + MemorySize: 512 + Timeout: 10 + Tracing: Active + Architectures: + - x86_64 + Environment: + Variables: + DYNAMODB_TABLE: !Ref ContractsTable + SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default + POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default + POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default + POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] # Metric Namespace + LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + Tags: + stage: !Ref Stage + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] + +#### RESOURCES +Resources: + #### SSM PARAMETERS + # Services own and share their namespace + UnicornContractsNamespaceParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + # Services share their event bus name and arn + UnicornContractsEventBusNameParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsEventBus + Value: !GetAtt UnicornContractsEventBus.Name + + UnicornContractsEventBusArnParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornContractsEventBusArn + Value: !GetAtt UnicornContractsEventBus.Arn + + #### LAMBDA FUNCTIONS + ContractEventHandlerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./ContractsService + Handler: Unicorn.Contracts.ContractService::Unicorn.Contracts.ContractService.ContractEventHandler::FunctionHandler + Policies: + - DynamoDBWritePolicy: + TableName: !Ref ContractsTable + - DynamoDBReadPolicy: + TableName: !Ref ContractsTable + Events: + IngestQueue: + Type: SQS + Properties: + Queue: !GetAtt UnicornContractsIngestQueue.Arn + BatchSize: 1 + Enabled: true + ScalingConfig: + MaximumConcurrency: 5 + + ContractEventHandlerFunctionLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + #### API GATEWAY REST API + UnicornContractsApi: + Type: AWS::Serverless::Api + DependsOn: ContractsApiGwAccountConfig + Properties: + StageName: !Ref Stage + EndpointConfiguration: + Type: REGIONAL + TracingEnabled: true + MethodSettings: + - MetricsEnabled: true + ResourcePath: /* + HttpMethod: "*" + LoggingLevel: !If [IsProd, ERROR, INFO] + ThrottlingBurstLimit: 10 + ThrottlingRateLimit: 100 + AccessLogSetting: + DestinationArn: !GetAtt UnicornContractsApiLogGroup.Arn + Format: !ToJsonString + requestId: $context.requestId + integration-error: $context.integration.error + integration-status: $context.integration.status + integration-latency: $context.integration.latency + integration-requestId: $context.integration.requestId + integration-integrationStatus: $context.integration.integrationStatus + response-latency: $context.responseLatency + status: $context.status + DefinitionBody: !Transform + Name: "AWS::Include" + Parameters: + Location: "api.yaml" + Tags: + stage: !Ref Stage + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] + + ContractsApiGwAccountConfig: + Type: AWS::ApiGateway::Account + Properties: + CloudWatchRoleArn: !GetAtt UnicornContractsApiGwAccountConfigRole.Arn + + # API GW IAM roles + ContractsApiIntegrationRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + Effect: Allow + Action: sts:AssumeRole + Principal: + Service: apigateway.amazonaws.com + Policies: + - PolicyName: AllowSqsIntegration + PolicyDocument: + Statement: + - Effect: Allow + Action: + - sqs:SendMessage + - sqs:GetQueueUrl + Resource: !GetAtt UnicornContractsIngestQueue.Arn + + # API GW Cloudwatch Log Group + UnicornContractsApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + UnicornContractsApiGwAccountConfigRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - apigateway.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + + #### INGEST QUEUES + UnicornContractsIngestQueue: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + RedrivePolicy: + deadLetterTargetArn: !GetAtt UnicornContractsIngestDLQ.Arn + maxReceiveCount: 1 + VisibilityTimeout: 20 + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + UnicornContractsIngestDLQ: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + cfn-lint: + config: + ignore_checks: + - ES6000 + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + #### DYNAMODB TABLE + ContractsTable: + Type: AWS::DynamoDB::Table + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + AttributeDefinitions: + - AttributeName: PropertyId + AttributeType: S + KeySchema: + - AttributeName: PropertyId + KeyType: HASH + StreamSpecification: + StreamViewType: NEW_AND_OLD_IMAGES + BillingMode: PAY_PER_REQUEST + Tags: + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + - Key: stage + Value: !Ref Stage + + #### EVENT BUS + # Event bus for Contract Service used to publish and consume events + UnicornContractsEventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Sub UnicornContractsBus-${Stage} + + # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) + ContractEventsBusPublishPolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Ref UnicornContractsEventBus + StatementId: !Sub ContactsPublishEventsPolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - events:PutEvents + Resource: !GetAtt UnicornContractsEventBus.Arn + Condition: + StringEquals: + events:source: + - !FindInMap [Constants, ServiceNamespace, Value] + + # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. + # Update this policy as you get new subscribers by adding their namespace to events:source + # PropertiesServiceRulePolicy: + # Type: "AWS::Events::EventBusPolicy" + # Properties: + # EventBusName: !Ref UnicornContractsEventBus + # StatementId: !Sub CreateRulePolicy-${Stage} + # Statement: + # Effect: Allow + # Principal: + # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + # Action: + # - events:PutRule + # - events:DeleteRule + # - events:DescribeRule + # - events:DisableRule + # - events:EnableRule + # - events:PutTargets + # - events:RemoveTargets + # Resource: + # - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornContractsEventBus.Name}/*" + # Condition: + # StringEqualsIfExists: + # "events:creatorAccount": "${aws:PrincipalAccount}" + # StringEquals: + # "events:source": + # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + # "Null": + # "events:source": "false" + + # Catchall rule used for development purposes. + UnicornContractsCatchAllRule: + Type: AWS::Events::Rule + Metadata: + cfn-lint: + config: + ignore_checks: + - ES4000 + Properties: + Name: contracts.catchall + Description: Catch all events published by the contracts service. + EventBusName: !Ref UnicornContractsEventBus + EventPattern: + source: + - prefix: "" + State: ENABLED #You may want to disable this rule in production + Targets: + - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn + Id: !Sub UnicornContractsCatchAllLogGroupTarget-${Stage} + + # CloudWatch log group used to catch all events + UnicornContractsCatchAllLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + # Permissions to allow EventBridge to send logs to CloudWatch + EventBridgeCloudWatchLogGroupPolicy: + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub EventBridgeToCWLogsPolicy-${Stage} + # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "delivery.logs.amazonaws.com", + "events.amazonaws.com" + ] + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "${UnicornContractsCatchAllLogGroup.Arn}" + ] + } + ] + } + + #### EVENT BRIDGE PIPES + # Pipe changed on DynamoDB Table to UnicornContractsEventBus + DdbStreamToEventPipe: + Type: AWS::Pipes::Pipe + Properties: + RoleArn: !GetAtt DdbStreamToEventPipeRole.Arn + Source: !GetAtt ContractsTable.StreamArn + SourceParameters: + DynamoDBStreamParameters: + StartingPosition: LATEST + OnPartialBatchItemFailure: AUTOMATIC_BISECT + BatchSize: 1 + # DeadLetterConfig: + # Arn: String + FilterCriteria: + Filters: + - Pattern: !ToJsonString + eventName: [INSERT, MODIFY] + dynamodb: + NewImage: + contract_status: + S: [DRAFT, APPROVED] + # Enrichment: arn:aws:execute-api:us-east-1:123456789123:53eo2i89p9/*/POST/pets + Target: !GetAtt UnicornContractsEventBus.Arn + TargetParameters: + EventBridgeEventBusParameters: + Source: !FindInMap [Constants, ServiceNamespace, Value] + DetailType: ContractStatusChanged + InputTemplate: !ToJsonString + PropertyId: "<$.dynamodb.NewImage.PropertyId.S>" + ContractId: "<$.dynamodb.NewImage.ContractId.S>" + ContractStatus: "<$.dynamodb.NewImage.ContractStatus.S>" + ContractLastModifiedOn: "<$.dynamodb.NewImage.ContractLastModifiedOn.S>" + + # IAM Role for Event Bridge Pipe + DdbStreamToEventPipeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + Action: sts:AssumeRole + Effect: Allow + Principal: + Service: pipes.amazonaws.com + Policies: + - PolicyName: AllowDdbStreamRead + PolicyDocument: + Statement: + - Effect: Allow + Action: + - dynamodb:ListStreams + Resource: "*" + - Effect: Allow + Action: + - dynamodb:DescribeStream + - dynamodb:GetRecords + - dynamodb:GetShardIterator + Resource: !GetAtt ContractsTable.StreamArn + - Effect: Allow + Action: + - events:PutEvents + Resource: !GetAtt UnicornContractsEventBus.Arn + +#### OUTPUTS +Outputs: + BaseUrl: + Description: Web service API endpoint + Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" + ApiUrl: + Description: Contract service API endpoint + Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + + IngestQueueUrl: + Description: URL for the Ingest SQS Queue + Value: !GetAtt UnicornContractsIngestQueue.QueueUrl + + ContractsTableName: + Description: DynamoDB table storing contract information + Value: !Ref ContractsTable + + ContractEventHandlerFunctionArn: + Description: Create Contract function ARN + Value: !GetAtt ContractEventHandlerFunction.Arn + + IsProd: + Description: Is Production? + Value: !If [IsProd, "true", "false"] diff --git a/unicorn_contracts/tests/integration/event_payloads/create_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/event_payloads/create_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/event_payloads/create_contract_valid_payload_1.json b/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json similarity index 66% rename from unicorn_contracts/tests/integration/event_payloads/create_contract_valid_payload_1.json rename to unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json index 7693fdf..8a9abf2 100644 --- a/unicorn_contracts/tests/integration/event_payloads/create_contract_valid_payload_1.json +++ b/unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json @@ -3,8 +3,8 @@ "country": "USA", "city": "Anytown", "street": "Main Street", - "number": 111 + "number": 123 }, "seller_name": "John Smith", - "property_id": "usa/anytown/main-street/111" + "property_id": "usa/anytown/main-street/123" } \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/event_payloads/update_existing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/event_payloads/update_existing_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/event_payloads/update_existing_contract_valid_payload.json b/unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/event_payloads/update_existing_contract_valid_payload.json rename to unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload_1.json diff --git a/unicorn_contracts/tests/integration/event_payloads/update_missing_contract_invalid_payload_1.json b/unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/event_payloads/update_missing_contract_invalid_payload_1.json rename to unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json diff --git a/unicorn_contracts/tests/integration/event_payloads/update_missing_contract_valid_payload.json b/unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload_1.json similarity index 100% rename from unicorn_contracts/tests/integration/event_payloads/update_missing_contract_valid_payload.json rename to unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload_1.json diff --git a/unicorn_contracts/tests/integration/transformations/ddb_contract.jq b/unicorn_contracts/tests/integration/transformations/ddb_contract.jq new file mode 100644 index 0000000..5f11110 --- /dev/null +++ b/unicorn_contracts/tests/integration/transformations/ddb_contract.jq @@ -0,0 +1,14 @@ +.Item | { + property_id: .property_id.S, + contract_id: .contract_id.S, + seller_name: .seller_name.S, + address: { + country: .address.M.country.S, + number: .address.M.number.N, + city: .address.M.city.S, + street: .address.M.street.S, + }, + contract_status: .contract_status.S, + contact_created: .contact_created.S, + contract_last_modified_on: .contract_last_modified_on.S +} | del(..|nulls) diff --git a/unicorn_contracts/tests/pipes/event_bridge_payloads/create_contract.json b/unicorn_contracts/tests/pipes/event_bridge_payloads/create_contract.json new file mode 100644 index 0000000..b40e720 --- /dev/null +++ b/unicorn_contracts/tests/pipes/event_bridge_payloads/create_contract.json @@ -0,0 +1,16 @@ +{ + "version": "0", + "id": "ab4c2d2e-f483-46e6-54b4-96a02950a556", + "detail-type": "ContractStatusChanged", + "source": "unicorn.contracts", + "account": "718758479978", + "time": "2023-08-25T04:59:40Z", + "region": "ap-southeast-2", + "resources": [], + "detail": { + "contract_id": "d63b341c-cf87-428d-b6ca-5e789bfdfc14", + "contract_last_modified_on": "25/08/2023 04:59:40", + "contract_status": "DRAFT", + "property_id": "usa/anytown/main-street/123" + } +} diff --git a/unicorn_contracts/tests/pipes/event_bridge_payloads/update_contract.json b/unicorn_contracts/tests/pipes/event_bridge_payloads/update_contract.json new file mode 100644 index 0000000..258b0f0 --- /dev/null +++ b/unicorn_contracts/tests/pipes/event_bridge_payloads/update_contract.json @@ -0,0 +1,16 @@ +{ + "version": "0", + "id": "0301025e-92e5-c036-534f-f5adbf8cb867", + "detail-type": "ContractStatusChanged", + "source": "unicorn.contracts", + "account": "718758479978", + "time": "2023-08-25T05:00:23Z", + "region": "ap-southeast-2", + "resources": [], + "detail": { + "contract_id": "d63b341c-cf87-428d-b6ca-5e789bfdfc14", + "contract_last_modified_on": "25/08/2023 04:59:40", + "contract_status": "APPROVED", + "property_id": "usa/anytown/main-street/123" + } +} diff --git a/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json b/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json new file mode 100644 index 0000000..0110b72 --- /dev/null +++ b/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json @@ -0,0 +1,55 @@ +{ + "eventID": "f596bdbb621c57e694189ae9f1c172c2", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "ap-southeast-2", + "dynamodb": { + "ApproximateCreationDateTime": 1692929660, + "Keys": { + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "NewImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "DRAFT" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "SequenceNumber": "4800600000000041815691506", + "SizeBytes": 303, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:718758479978:table/uni-prop-local-contract-ContractsTable-JKAROODQJH0P/stream/2023-08-24T00:35:44.603" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json b/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json new file mode 100644 index 0000000..e6f8788 --- /dev/null +++ b/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json @@ -0,0 +1,94 @@ +{ + "eventID": "54e72fec04d65113c9bc5905a3e3a18c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "ap-southeast-2", + "dynamodb": { + "ApproximateCreationDateTime": 1692929694, + "Keys": { + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "NewImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "APPROVED" + }, + "modified_date": { + "S": "25/08/2023 02:14:54" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "OldImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "DRAFT" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "SequenceNumber": "4800700000000041815709335", + "SizeBytes": 603, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:718758479978:table/uni-prop-local-contract-ContractsTable-JKAROODQJH0P/stream/2023-08-24T00:35:44.603" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json new file mode 100644 index 0000000..856608b --- /dev/null +++ b/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json @@ -0,0 +1,47 @@ +{ + "ApproximateCreationDateTime": 1692929660, + "Keys": { + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "NewImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "DRAFT" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "SequenceNumber": "4800600000000041815691506", + "SizeBytes": 303, + "StreamViewType": "NEW_AND_OLD_IMAGES" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json new file mode 100644 index 0000000..bf8b0e6 --- /dev/null +++ b/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json @@ -0,0 +1,50 @@ +{ + "ApproximateCreationDateTime": 1692929729, + "Keys": { + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "OldImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "APPROVED" + }, + "modified_date": { + "S": "25/08/2023 02:14:54" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "SequenceNumber": "4800800000000041815727252", + "SizeBytes": 338, + "StreamViewType": "NEW_AND_OLD_IMAGES" +} \ No newline at end of file diff --git a/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json new file mode 100644 index 0000000..44bb357 --- /dev/null +++ b/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json @@ -0,0 +1,86 @@ +{ + "ApproximateCreationDateTime": 1692929694, + "Keys": { + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "NewImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "APPROVED" + }, + "modified_date": { + "S": "25/08/2023 02:14:54" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "OldImage": { + "contract_last_modified_on": { + "S": "25/08/2023 02:14:20" + }, + "address": { + "M": { + "country": { + "S": "USA" + }, + "number": { + "N": "123" + }, + "city": { + "S": "Anytown" + }, + "street": { + "S": "Main Street" + } + } + }, + "seller_name": { + "S": "John Smith" + }, + "contact_created": { + "S": "25/08/2023 02:14:20" + }, + "contract_id": { + "S": "5bb04023-74aa-41fc-b86b-447602759270" + }, + "contract_status": { + "S": "DRAFT" + }, + "property_id": { + "S": "usa/anytown/main-street/123" + } + }, + "SequenceNumber": "4800700000000041815709335", + "SizeBytes": 603, + "StreamViewType": "NEW_AND_OLD_IMAGES" +} \ No newline at end of file From 5422ed182854f1a90598b34f09c7f2a8a36229e9 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 25 Aug 2023 15:27:45 +1000 Subject: [PATCH 07/87] bug: remove file added by mistake --- unicorn_contracts/template_steve.yaml | 474 -------------------------- 1 file changed, 474 deletions(-) delete mode 100644 unicorn_contracts/template_steve.yaml diff --git a/unicorn_contracts/template_steve.yaml b/unicorn_contracts/template_steve.yaml deleted file mode 100644 index 3962897..0000000 --- a/unicorn_contracts/template_steve.yaml +++ /dev/null @@ -1,474 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -AWSTemplateFormatVersion: 2010-09-09 -Transform: - - AWS::LanguageExtensions - - AWS::Serverless-2016-10-31 -Description: > - Unicorn Contracts Service resources. - -#### METADATA -Metadata: - cfn-lint: - config: - ignore_checks: - - I3042 - - ES1007 - - ES1001 - - WS2001 - -#### PARAMETERS -Parameters: - Stage: - Type: String - Default: Local - AllowedValues: - - Local - - Dev - - Prod - -#### MAPPINGS -Mappings: - LogsRetentionPeriodMap: - Local: - Days: 3 - Dev: - Days: 3 - Prod: - Days: 14 - Constants: - ServiceNamespace: - Value: "unicorn.contracts" - ProjectName: - Value: "AWS Serverless Developer Experience" - -#### CONDITIONS -Conditions: - IsProd: !Equals [!Ref Stage, Prod] - -#### GLOBALS -Globals: - Api: - OpenApiVersion: 3.0.1 - Function: - Runtime: dotnet6 - MemorySize: 512 - Timeout: 10 - Tracing: Active - Architectures: - - x86_64 - Environment: - Variables: - DYNAMODB_TABLE: !Ref ContractsTable - SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] - POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] - POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default - POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default - POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] # Metric Namespace - LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default - Tags: - stage: !Ref Stage - project: !FindInMap [Constants, ProjectName, Value] - namespace: !FindInMap [Constants, ServiceNamespace, Value] - -#### RESOURCES -Resources: - #### SSM PARAMETERS - # Services own and share their namespace - UnicornContractsNamespaceParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsNamespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - - # Services share their event bus name and arn - UnicornContractsEventBusNameParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsEventBus - Value: !GetAtt UnicornContractsEventBus.Name - - UnicornContractsEventBusArnParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsEventBusArn - Value: !GetAtt UnicornContractsEventBus.Arn - - #### LAMBDA FUNCTIONS - ContractEventHandlerFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: ./ContractsService - Handler: Unicorn.Contracts.ContractService::Unicorn.Contracts.ContractService.ContractEventHandler::FunctionHandler - Policies: - - DynamoDBWritePolicy: - TableName: !Ref ContractsTable - - DynamoDBReadPolicy: - TableName: !Ref ContractsTable - Events: - IngestQueue: - Type: SQS - Properties: - Queue: !GetAtt UnicornContractsIngestQueue.Arn - BatchSize: 1 - Enabled: true - ScalingConfig: - MaximumConcurrency: 5 - - ContractEventHandlerFunctionLogGroup: - Type: AWS::Logs::LogGroup - DeletionPolicy: Delete - UpdateReplacePolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - #### API GATEWAY REST API - UnicornContractsApi: - Type: AWS::Serverless::Api - DependsOn: ContractsApiGwAccountConfig - Properties: - StageName: !Ref Stage - EndpointConfiguration: - Type: REGIONAL - TracingEnabled: true - MethodSettings: - - MetricsEnabled: true - ResourcePath: /* - HttpMethod: "*" - LoggingLevel: !If [IsProd, ERROR, INFO] - ThrottlingBurstLimit: 10 - ThrottlingRateLimit: 100 - AccessLogSetting: - DestinationArn: !GetAtt UnicornContractsApiLogGroup.Arn - Format: !ToJsonString - requestId: $context.requestId - integration-error: $context.integration.error - integration-status: $context.integration.status - integration-latency: $context.integration.latency - integration-requestId: $context.integration.requestId - integration-integrationStatus: $context.integration.integrationStatus - response-latency: $context.responseLatency - status: $context.status - DefinitionBody: !Transform - Name: "AWS::Include" - Parameters: - Location: "api.yaml" - Tags: - stage: !Ref Stage - project: !FindInMap [Constants, ProjectName, Value] - namespace: !FindInMap [Constants, ServiceNamespace, Value] - - ContractsApiGwAccountConfig: - Type: AWS::ApiGateway::Account - Properties: - CloudWatchRoleArn: !GetAtt UnicornContractsApiGwAccountConfigRole.Arn - - # API GW IAM roles - ContractsApiIntegrationRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - Effect: Allow - Action: sts:AssumeRole - Principal: - Service: apigateway.amazonaws.com - Policies: - - PolicyName: AllowSqsIntegration - PolicyDocument: - Statement: - - Effect: Allow - Action: - - sqs:SendMessage - - sqs:GetQueueUrl - Resource: !GetAtt UnicornContractsIngestQueue.Arn - - # API GW Cloudwatch Log Group - UnicornContractsApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - UnicornContractsApiGwAccountConfigRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Effect: Allow - Principal: - Service: - - apigateway.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs - - #### INGEST QUEUES - UnicornContractsIngestQueue: - Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - RedrivePolicy: - deadLetterTargetArn: !GetAtt UnicornContractsIngestDLQ.Arn - maxReceiveCount: 1 - VisibilityTimeout: 20 - Tags: - - Key: stage - Value: !Ref Stage - - Key: project - Value: !FindInMap [Constants, ProjectName, Value] - - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - - UnicornContractsIngestDLQ: - Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Metadata: - cfn-lint: - config: - ignore_checks: - - ES6000 - Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - Tags: - - Key: stage - Value: !Ref Stage - - Key: project - Value: !FindInMap [Constants, ProjectName, Value] - - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - - #### DYNAMODB TABLE - ContractsTable: - Type: AWS::DynamoDB::Table - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - AttributeDefinitions: - - AttributeName: PropertyId - AttributeType: S - KeySchema: - - AttributeName: PropertyId - KeyType: HASH - StreamSpecification: - StreamViewType: NEW_AND_OLD_IMAGES - BillingMode: PAY_PER_REQUEST - Tags: - - Key: project - Value: !FindInMap [Constants, ProjectName, Value] - - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - - Key: stage - Value: !Ref Stage - - #### EVENT BUS - # Event bus for Contract Service used to publish and consume events - UnicornContractsEventBus: - Type: AWS::Events::EventBus - Properties: - Name: !Sub UnicornContractsBus-${Stage} - - # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) - ContractEventsBusPublishPolicy: - Type: AWS::Events::EventBusPolicy - Properties: - EventBusName: !Ref UnicornContractsEventBus - StatementId: !Sub ContactsPublishEventsPolicy-${Stage} - Statement: - Effect: Allow - Principal: - AWS: - - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - Action: - - events:PutEvents - Resource: !GetAtt UnicornContractsEventBus.Arn - Condition: - StringEquals: - events:source: - - !FindInMap [Constants, ServiceNamespace, Value] - - # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. - # Update this policy as you get new subscribers by adding their namespace to events:source - # PropertiesServiceRulePolicy: - # Type: "AWS::Events::EventBusPolicy" - # Properties: - # EventBusName: !Ref UnicornContractsEventBus - # StatementId: !Sub CreateRulePolicy-${Stage} - # Statement: - # Effect: Allow - # Principal: - # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - # Action: - # - events:PutRule - # - events:DeleteRule - # - events:DescribeRule - # - events:DisableRule - # - events:EnableRule - # - events:PutTargets - # - events:RemoveTargets - # Resource: - # - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornContractsEventBus.Name}/*" - # Condition: - # StringEqualsIfExists: - # "events:creatorAccount": "${aws:PrincipalAccount}" - # StringEquals: - # "events:source": - # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - # "Null": - # "events:source": "false" - - # Catchall rule used for development purposes. - UnicornContractsCatchAllRule: - Type: AWS::Events::Rule - Metadata: - cfn-lint: - config: - ignore_checks: - - ES4000 - Properties: - Name: contracts.catchall - Description: Catch all events published by the contracts service. - EventBusName: !Ref UnicornContractsEventBus - EventPattern: - source: - - prefix: "" - State: ENABLED #You may want to disable this rule in production - Targets: - - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn - Id: !Sub UnicornContractsCatchAllLogGroupTarget-${Stage} - - # CloudWatch log group used to catch all events - UnicornContractsCatchAllLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - # Permissions to allow EventBridge to send logs to CloudWatch - EventBridgeCloudWatchLogGroupPolicy: - Type: AWS::Logs::ResourcePolicy - Properties: - PolicyName: !Sub EventBridgeToCWLogsPolicy-${Stage} - # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument - PolicyDocument: !Sub | - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "delivery.logs.amazonaws.com", - "events.amazonaws.com" - ] - }, - "Action": [ - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": [ - "${UnicornContractsCatchAllLogGroup.Arn}" - ] - } - ] - } - - #### EVENT BRIDGE PIPES - # Pipe changed on DynamoDB Table to UnicornContractsEventBus - DdbStreamToEventPipe: - Type: AWS::Pipes::Pipe - Properties: - RoleArn: !GetAtt DdbStreamToEventPipeRole.Arn - Source: !GetAtt ContractsTable.StreamArn - SourceParameters: - DynamoDBStreamParameters: - StartingPosition: LATEST - OnPartialBatchItemFailure: AUTOMATIC_BISECT - BatchSize: 1 - # DeadLetterConfig: - # Arn: String - FilterCriteria: - Filters: - - Pattern: !ToJsonString - eventName: [INSERT, MODIFY] - dynamodb: - NewImage: - contract_status: - S: [DRAFT, APPROVED] - # Enrichment: arn:aws:execute-api:us-east-1:123456789123:53eo2i89p9/*/POST/pets - Target: !GetAtt UnicornContractsEventBus.Arn - TargetParameters: - EventBridgeEventBusParameters: - Source: !FindInMap [Constants, ServiceNamespace, Value] - DetailType: ContractStatusChanged - InputTemplate: !ToJsonString - PropertyId: "<$.dynamodb.NewImage.PropertyId.S>" - ContractId: "<$.dynamodb.NewImage.ContractId.S>" - ContractStatus: "<$.dynamodb.NewImage.ContractStatus.S>" - ContractLastModifiedOn: "<$.dynamodb.NewImage.ContractLastModifiedOn.S>" - - # IAM Role for Event Bridge Pipe - DdbStreamToEventPipeRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: pipes.amazonaws.com - Policies: - - PolicyName: AllowDdbStreamRead - PolicyDocument: - Statement: - - Effect: Allow - Action: - - dynamodb:ListStreams - Resource: "*" - - Effect: Allow - Action: - - dynamodb:DescribeStream - - dynamodb:GetRecords - - dynamodb:GetShardIterator - Resource: !GetAtt ContractsTable.StreamArn - - Effect: Allow - Action: - - events:PutEvents - Resource: !GetAtt UnicornContractsEventBus.Arn - -#### OUTPUTS -Outputs: - BaseUrl: - Description: Web service API endpoint - Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" - ApiUrl: - Description: Contract service API endpoint - Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" - - IngestQueueUrl: - Description: URL for the Ingest SQS Queue - Value: !GetAtt UnicornContractsIngestQueue.QueueUrl - - ContractsTableName: - Description: DynamoDB table storing contract information - Value: !Ref ContractsTable - - ContractEventHandlerFunctionArn: - Description: Create Contract function ARN - Value: !GetAtt ContractEventHandlerFunction.Arn - - IsProd: - Description: Is Production? - Value: !If [IsProd, "true", "false"] From 04d5933ff3cca956dced68cefcf5fb6221654d81 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 25 Aug 2023 17:38:09 +1000 Subject: [PATCH 08/87] feat: Refactored unicorn_web --- unicorn_contracts/api.yaml | 8 +- .../src/contracts_service/enums.py | 7 +- unicorn_contracts/template.yaml | 32 +- unicorn_properties/poetry.lock | 228 ++++---- unicorn_properties/pyproject.toml | 5 +- unicorn_web/Makefile | 2 +- unicorn_web/api.yaml | 199 +++++++ unicorn_web/data/load_data.sh | 5 +- unicorn_web/poetry.lock | 228 ++++---- unicorn_web/pyproject.toml | 4 +- unicorn_web/samconfig.toml | 16 - unicorn_web/samconfig.yaml | 43 ++ unicorn_web/subscriptions.yaml | 78 +++ unicorn_web/template.yaml | 547 +++++++++++++----- 14 files changed, 948 insertions(+), 454 deletions(-) create mode 100644 unicorn_web/api.yaml delete mode 100644 unicorn_web/samconfig.toml create mode 100644 unicorn_web/samconfig.yaml create mode 100644 unicorn_web/subscriptions.yaml diff --git a/unicorn_contracts/api.yaml b/unicorn_contracts/api.yaml index 936bee7..4bf305d 100644 --- a/unicorn_contracts/api.yaml +++ b/unicorn_contracts/api.yaml @@ -21,8 +21,8 @@ paths: $ref: "#/components/schemas/Empty" x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: - credentials: - Fn::GetAtt: [ ContractsApiIntegrationRole, Arn ] + credentials: + Fn::GetAtt: [ContractsApiIntegrationRole, Arn] httpMethod: "POST" uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" @@ -83,8 +83,8 @@ paths: $ref: "#/components/schemas/Empty" x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: - credentials: - Fn::GetAtt: [ ContractsApiIntegrationRole, Arn ] + credentials: + Fn::GetAtt: [ContractsApiIntegrationRole, Arn] httpMethod: "POST" uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" diff --git a/unicorn_contracts/src/contracts_service/enums.py b/unicorn_contracts/src/contracts_service/enums.py index fa2b4d7..a9f8ee7 100644 --- a/unicorn_contracts/src/contracts_service/enums.py +++ b/unicorn_contracts/src/contracts_service/enums.py @@ -9,9 +9,12 @@ class ContractStatus(Enum): APPROVED The contract record is approved. CANCELLED The contract record is canceled or terminated. You cannot modify a contract record that has this status value. - CLOSED The contract record is closed and all its terms and conditions are met. You cannot modify a contract record that has this status value. + CLOSED The contract record is closed and all its terms and conditions are met. + You cannot modify a contract record that has this status value. DRAFT The contract is a draft. - EXPIRED The contract record is expired. The end date for the contract has passed. You cannot modify a contract record that has this status value. You can change the status from expire to pending revision by revising the expired contract. + EXPIRED The contract record is expired. The end date for the contract has passed. + You cannot modify a contract record that has this status value. + You can change the status from expire to pending revision by revising the expired contract. Parameters ---------- diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 12e8a31..38e38eb 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -302,36 +302,6 @@ Resources: events:source: - !FindInMap [Constants, ServiceNamespace, Value] - # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. - # Update this policy as you get new subscribers by adding their namespace to events:source - # PropertiesServiceRulePolicy: - # Type: "AWS::Events::EventBusPolicy" - # Properties: - # EventBusName: !Ref UnicornContractsEventBus - # StatementId: !Sub CreateRulePolicy-${Stage} - # Statement: - # Effect: Allow - # Principal: - # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - # Action: - # - events:PutRule - # - events:DeleteRule - # - events:DescribeRule - # - events:DisableRule - # - events:EnableRule - # - events:PutTargets - # - events:RemoveTargets - # Resource: - # - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornContractsEventBus.Name}/*" - # Condition: - # StringEqualsIfExists: - # "events:creatorAccount": "${aws:PrincipalAccount}" - # StringEquals: - # "events:source": - # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - # "Null": - # "events:source": "false" - # Catchall rule used for development purposes. UnicornContractsCatchAllRule: Type: AWS::Events::Rule @@ -364,7 +334,7 @@ Resources: EventBridgeCloudWatchLogGroupPolicy: Type: AWS::Logs::ResourcePolicy Properties: - PolicyName: !Sub EventBridgeToCWLogsPolicy-${Stage} + PolicyName: !Sub UnicornContracts-EventBridgeToCWLogsPolicy-${Stage} # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument PolicyDocument: !Sub | { diff --git a/unicorn_properties/poetry.lock b/unicorn_properties/poetry.lock index d9a61ac..3c7c431 100644 --- a/unicorn_properties/poetry.lock +++ b/unicorn_properties/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "aws-lambda-powertools" -version = "2.22.0" +version = "2.23.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.22.0-py3-none-any.whl", hash = "sha256:eae1f1c961893dab5d1e75ffb44d9b58f6426cb148aa39413b04cf36ae46fbe3"}, - {file = "aws_lambda_powertools-2.22.0.tar.gz", hash = "sha256:0fd535251454b1bd68dbff65e3ed56aa567f3841011e2afbd557b125596a6814"}, + {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, + {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, ] [package.dependencies] @@ -39,17 +39,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.15" +version = "1.28.32" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.15-py3-none-any.whl", hash = "sha256:84b7952858e9319968b0348d9894a91a6bb5f31e81a45c68044d040a12362abe"}, - {file = "boto3-1.28.15.tar.gz", hash = "sha256:a6e711e0b6960c3a5b789bd30c5a18eea7263f2a59fc07f85efa5e04804e49d2"}, + {file = "boto3-1.28.32-py3-none-any.whl", hash = "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3"}, + {file = "boto3-1.28.32.tar.gz", hash = "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28"}, ] [package.dependencies] -botocore = ">=1.31.15,<1.32.0" +botocore = ">=1.31.32,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +58,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.15" +version = "1.31.32" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.15-py3-none-any.whl", hash = "sha256:b3a0f787f275711875476cbe12a0123b2e6570b2f505e2fa509dcec3c5410b57"}, - {file = "botocore-1.31.15.tar.gz", hash = "sha256:b46d1ce4e0cf42d28fdf61ce0c999904645d38b51cb809817a361c0cec16d487"}, + {file = "botocore-1.31.32-py3-none-any.whl", hash = "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c"}, + {file = "botocore-1.31.32.tar.gz", hash = "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664"}, ] [package.dependencies] @@ -259,71 +259,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, + {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, + {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, + {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, + {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, + {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, + {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, + {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, + {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, + {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, + {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, + {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, + {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, ] [package.extras] @@ -331,34 +323,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -504,13 +496,13 @@ files = [ [[package]] name = "moto" -version = "4.1.13" +version = "4.1.15" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.1.13-py2.py3-none-any.whl", hash = "sha256:9650d05d89b6f97043695548fbc0d8fb293f4177daaebbcee00bb0d171367f1a"}, - {file = "moto-4.1.13.tar.gz", hash = "sha256:dd3e2ad920ab8b058c4f62fa7c195b788bd1f018cc701a1868ff5d5c4de6ed47"}, + {file = "moto-4.1.15-py2.py3-none-any.whl", hash = "sha256:3fbcf91090692c30117d275fb34b48a075a6f65d4712ba6c4d004ffab976db46"}, + {file = "moto-4.1.15.tar.gz", hash = "sha256:272236d312457b324c645741ee589924fd61a96b84680dc2e607f8663c563551"}, ] [package.dependencies] @@ -525,26 +517,28 @@ werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" xmltodict = "*" [package.extras] -all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] apigateway = ["PyYAML (>=5.1)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.2.8)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] apigatewayv2 = ["PyYAML (>=5.1)"] appsync = ["graphql-core"] awslambda = ["docker (>=3.0.0)"] batch = ["docker (>=3.0.0)"] -cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] cognitoidp = ["ecdsa (!=0.15)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] ds = ["sshpubkeys (>=3.1.0)"] -dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] -dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] ebs = ["sshpubkeys (>=3.1.0)"] ec2 = ["sshpubkeys (>=3.1.0)"] efs = ["sshpubkeys (>=3.1.0)"] eks = ["sshpubkeys (>=3.1.0)"] glue = ["pyparsing (>=3.0.7)"] iotdata = ["jsondiff (>=1.1.2)"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "sshpubkeys (>=3.1.0)"] route53resolver = ["sshpubkeys (>=3.1.0)"] -s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.3)"] -server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.6)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.3.6)"] +server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] ssm = ["PyYAML (>=5.1)"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] @@ -726,33 +720,33 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.23.1" +version = "0.23.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, - {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, ] [package.dependencies] pyyaml = "*" -requests = ">=2.22.0,<3.0" +requests = ">=2.30.0,<3.0" types-PyYAML = "*" -urllib3 = ">=1.25.10" +urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] [[package]] name = "s3transfer" -version = "0.6.1" +version = "0.6.2" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.7" files = [ - {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, - {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, + {file = "s3transfer-0.6.2-py3-none-any.whl", hash = "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084"}, + {file = "s3transfer-0.6.2.tar.gz", hash = "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"}, ] [package.dependencies] @@ -812,13 +806,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "werkzeug" -version = "2.3.6" +version = "2.3.7" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, - {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, + {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"}, + {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"}, ] [package.dependencies] @@ -940,4 +934,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d0163f802a4318ec92b5273a56c206db96edfd7f4b8e88165e0e47056e62cdfa" +content-hash = "ea89392c5b9832d401f61ec43f03d41d3f2b7efb94932263c007084f872b5aa7" diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index eeff265..cf125b2 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -10,8 +10,8 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.15" -aws-lambda-powertools = {extras = ["tracer"], version = "^2.22.0"} +boto3 = "^1.28.32" +aws-lambda-powertools = {extras = ["tracer"], version = "^2.23.0"} aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] @@ -22,6 +22,7 @@ coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" +pyyaml = "^6.0.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/unicorn_web/Makefile b/unicorn_web/Makefile index edba753..d6c79ae 100644 --- a/unicorn_web/Makefile +++ b/unicorn_web/Makefile @@ -2,7 +2,7 @@ deps: poetry install build: - # cfn-lint template.yaml -a cfn_lint_serverless.rules + cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export -f requirements.txt --without-hashes --output src/approvals_service/requirements.txt poetry export -f requirements.txt --without-hashes --output src/search_service/requirements.txt sam build -c $(DOCKER_OPTS) diff --git a/unicorn_web/api.yaml b/unicorn_web/api.yaml new file mode 100644 index 0000000..ae530c3 --- /dev/null +++ b/unicorn_web/api.yaml @@ -0,0 +1,199 @@ +openapi: "3.0.1" +info: + title: "Unicorn Web API" + version: "1.0.0" + description: Unicorn Properties Web Service API +paths: + /request_approval: + post: + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PublicationEvaluationRequestModel" + required: true + responses: + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-request-validator: "Validate body" + x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] + httpMethod: "POST" + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornWebIngestQueue.QueueName}" + responses: + default: + statusCode: "200" + responseTemplates: + application/json: '{"message":"OK"}' + requestParameters: + integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" + requestTemplates: + application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" + passthroughBehavior: "never" + type: "aws" + options: + responses: + "200": + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + responseParameters: + method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Origin: "'*'" + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: "when_no_match" + type: "mock" + /search/{country}/{city}: + get: + parameters: + - name: "country" + in: "path" + required: true + schema: + type: "string" + - name: "city" + in: "path" + required: true + schema: + type: "string" + x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] + httpMethod: "POST" + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SearchFunction.Arn}/invocations" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_match" + contentHandling: "CONVERT_TO_TEXT" + type: "aws_proxy" + options: + responses: + "200": + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + responseParameters: + method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Origin: "'*'" + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: "when_no_match" + type: "mock" + /search/{country}/{city}/{street}: + get: + parameters: + - name: "street" + in: "path" + required: true + schema: + type: "string" + - name: "country" + in: "path" + required: true + schema: + type: "string" + - name: "city" + in: "path" + required: true + schema: + type: "string" + x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] + httpMethod: "POST" + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SearchFunction.Arn}/invocations" + passthroughBehavior: "when_no_match" + type: "aws_proxy" + options: + responses: + "200": + description: "200 response" + headers: + Access-Control-Allow-Origin: + schema: + type: "string" + Access-Control-Allow-Methods: + schema: + type: "string" + Access-Control-Allow-Headers: + schema: + type: "string" + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + responseParameters: + method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Origin: "'*'" + requestTemplates: + application/json: '{"statusCode": 200}' + passthroughBehavior: "when_no_match" + type: "mock" +components: + schemas: + PublicationEvaluationRequestModel: + required: + - "property_id" + type: "object" + properties: + property_id: + type: "string" + PublicationEvaluationResponseModel: + required: + - "result" + type: "object" + properties: + result: + type: "string" + Empty: + title: "Empty Schema" + type: "object" \ No newline at end of file diff --git a/unicorn_web/data/load_data.sh b/unicorn_web/data/load_data.sh index 830d56f..07c22d5 100755 --- a/unicorn_web/data/load_data.sh +++ b/unicorn_web/data/load_data.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -STACK_NAME="uni-prop-local-web" +ROOT_DIR="$(cd -- "$(dirname "$0")/../" >/dev/null 2>&1 ; pwd -P )" +STACK_NAME="$(yq -oy '.default.global.parameters.stack_name' $ROOT_DIR/samconfig.yaml)" -JSON_FILE="$(cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/property_data.json" +JSON_FILE="$(cd -- "$ROOT_DIR/data/property_data.json" echo "JSON_FILE: '${JSON_FILE}'" DDB_TBL_NAME="$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query 'Stacks[0].Outputs[?OutputKey==`WebTableName`].OutputValue' --output text)" diff --git a/unicorn_web/poetry.lock b/unicorn_web/poetry.lock index f62b57c..cb113f4 100644 --- a/unicorn_web/poetry.lock +++ b/unicorn_web/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "aws-lambda-powertools" -version = "2.22.0" +version = "2.23.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.22.0-py3-none-any.whl", hash = "sha256:eae1f1c961893dab5d1e75ffb44d9b58f6426cb148aa39413b04cf36ae46fbe3"}, - {file = "aws_lambda_powertools-2.22.0.tar.gz", hash = "sha256:0fd535251454b1bd68dbff65e3ed56aa567f3841011e2afbd557b125596a6814"}, + {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, + {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, ] [package.dependencies] @@ -39,17 +39,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.15" +version = "1.28.32" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.15-py3-none-any.whl", hash = "sha256:84b7952858e9319968b0348d9894a91a6bb5f31e81a45c68044d040a12362abe"}, - {file = "boto3-1.28.15.tar.gz", hash = "sha256:a6e711e0b6960c3a5b789bd30c5a18eea7263f2a59fc07f85efa5e04804e49d2"}, + {file = "boto3-1.28.32-py3-none-any.whl", hash = "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3"}, + {file = "boto3-1.28.32.tar.gz", hash = "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28"}, ] [package.dependencies] -botocore = ">=1.31.15,<1.32.0" +botocore = ">=1.31.32,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +58,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.15" +version = "1.31.32" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.15-py3-none-any.whl", hash = "sha256:b3a0f787f275711875476cbe12a0123b2e6570b2f505e2fa509dcec3c5410b57"}, - {file = "botocore-1.31.15.tar.gz", hash = "sha256:b46d1ce4e0cf42d28fdf61ce0c999904645d38b51cb809817a361c0cec16d487"}, + {file = "botocore-1.31.32-py3-none-any.whl", hash = "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c"}, + {file = "botocore-1.31.32.tar.gz", hash = "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664"}, ] [package.dependencies] @@ -259,71 +259,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, + {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, + {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, + {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, + {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, + {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, + {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, + {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, + {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, + {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, + {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, + {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, + {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, ] [package.extras] @@ -342,34 +334,34 @@ files = [ [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -515,13 +507,13 @@ files = [ [[package]] name = "moto" -version = "4.1.13" +version = "4.1.15" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.1.13-py2.py3-none-any.whl", hash = "sha256:9650d05d89b6f97043695548fbc0d8fb293f4177daaebbcee00bb0d171367f1a"}, - {file = "moto-4.1.13.tar.gz", hash = "sha256:dd3e2ad920ab8b058c4f62fa7c195b788bd1f018cc701a1868ff5d5c4de6ed47"}, + {file = "moto-4.1.15-py2.py3-none-any.whl", hash = "sha256:3fbcf91090692c30117d275fb34b48a075a6f65d4712ba6c4d004ffab976db46"}, + {file = "moto-4.1.15.tar.gz", hash = "sha256:272236d312457b324c645741ee589924fd61a96b84680dc2e607f8663c563551"}, ] [package.dependencies] @@ -536,26 +528,28 @@ werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" xmltodict = "*" [package.extras] -all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] apigateway = ["PyYAML (>=5.1)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.2.8)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] apigatewayv2 = ["PyYAML (>=5.1)"] appsync = ["graphql-core"] awslambda = ["docker (>=3.0.0)"] batch = ["docker (>=3.0.0)"] -cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] cognitoidp = ["ecdsa (!=0.15)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] ds = ["sshpubkeys (>=3.1.0)"] -dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] -dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.3)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.3.6)"] ebs = ["sshpubkeys (>=3.1.0)"] ec2 = ["sshpubkeys (>=3.1.0)"] efs = ["sshpubkeys (>=3.1.0)"] eks = ["sshpubkeys (>=3.1.0)"] glue = ["pyparsing (>=3.0.7)"] iotdata = ["jsondiff (>=1.1.2)"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "sshpubkeys (>=3.1.0)"] route53resolver = ["sshpubkeys (>=3.1.0)"] -s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.3)"] -server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.3)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.3.6)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.3.6)"] +server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.2.8)", "py-partiql-parser (==0.3.6)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] ssm = ["PyYAML (>=5.1)"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] @@ -737,33 +731,33 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.23.1" +version = "0.23.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, - {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, ] [package.dependencies] pyyaml = "*" -requests = ">=2.22.0,<3.0" +requests = ">=2.30.0,<3.0" types-PyYAML = "*" -urllib3 = ">=1.25.10" +urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] [[package]] name = "s3transfer" -version = "0.6.1" +version = "0.6.2" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.7" files = [ - {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, - {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, + {file = "s3transfer-0.6.2-py3-none-any.whl", hash = "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084"}, + {file = "s3transfer-0.6.2.tar.gz", hash = "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"}, ] [package.dependencies] @@ -823,13 +817,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "werkzeug" -version = "2.3.6" +version = "2.3.7" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, - {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, + {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"}, + {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"}, ] [package.dependencies] @@ -951,4 +945,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "af141c1b17a67e9c4933e5c90590dedb691e3484ebb0cf551f778f690163d504" +content-hash = "99c790c06737375a60aad7fa149e27f638d3dbe2f14389872c1a70164237f480" diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index c9e9193..183191b 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -10,8 +10,8 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.15" -aws-lambda-powertools = {extras = ["tracer"], version = "^2.22.0"} +boto3 = "^1.28.32" +aws-lambda-powertools = {extras = ["tracer"], version = "^2.23.0"} aws-xray-sdk = "^2.12.0" requests = "2.31.0" crhelper = "^2.0.11" diff --git a/unicorn_web/samconfig.toml b/unicorn_web/samconfig.toml deleted file mode 100644 index f5ea2ce..0000000 --- a/unicorn_web/samconfig.toml +++ /dev/null @@ -1,16 +0,0 @@ -version = 0.1 -[default] -[default.deploy] -[default.deploy.parameters] -disable_rollback = true -stack_name = "uni-prop-local-web" -s3_prefix = "uni-prop-local-web" -capabilities = "CAPABILITY_IAM" -parameter_overrides = "Stage=\"Local\"" -resolve_s3 = true -resolve_image_repositories = true - -[default.delete] -[default.delete.parameters] -stack_name = "uni-prop-local-web" -no_prompts = true diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml new file mode 100644 index 0000000..928d97c --- /dev/null +++ b/unicorn_web/samconfig.yaml @@ -0,0 +1,43 @@ +version: 0.1 +default: + global: + parameters: + stack_name: "uni-prop-local-web" + s3_prefix: "uni-prop-local-web" + resolve_s3: true + resolve_image_repositories: true + build: + parameters: + cached: true + parallel: true + deploy: + parameters: + confirm_changeset: false + capabilities: CAPABILITY_IAM + disable_rollback: true + parameter_overrides: Stage="Local" + image_repositories: [] + validate: + parameters: + lint: true + sync: + parameters: + watch: true + local_start_api: + parameters: + warm_containers: "EAGER" + local_start_lambda: + parameters: + warm_containers: "EAGER" + +subscriptions: + global: + parameters: + stack_name: uni-prop-local-web-subscriptions + deploy: + parameters: + parameter_overrides: Stage="Local" + capabilities: CAPABILITY_IAM + template_file: "subscriptions.yaml" + fail_on_empty_changeset: false + on_failure: ROLLBACK \ No newline at end of file diff --git a/unicorn_web/subscriptions.yaml b/unicorn_web/subscriptions.yaml new file mode 100644 index 0000000..bd1963b --- /dev/null +++ b/unicorn_web/subscriptions.yaml @@ -0,0 +1,78 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: "2010-09-09" +Description: Defines EventBus subscriptions for UnicornWeb service + +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +Resources: + PublicationApprovedSubscriptionRule: + Type: AWS::Events::Rule + Properties: + Name: web.publicationapproved-properties.pubevalcompleted + Description: Publication evaluation completed subscription + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + EventPattern: + source: + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespaceParam}}" + detail-type: + - PublicationEvaluationCompleted + State: ENABLED + Targets: + - Id: SendEventTo + Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + RoleArn: !GetAtt UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn + + # This IAM role allows EventBridge to assume the permissions necessary to send events + # from the Unicorn Properties event bus to the Unicorn Web event bus. + # If the Unicorn Web wants receive events from other services, then add another policy + # the arn of the which includes the event bus arn where Unicorn Web is creating the + # subscription (rule) + UnicornPropertiesEventBusToUnicornWebEventBusRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: PutEventsOnUnicornWebEventBus + PolicyDocument: + Statement: + - Effect: Allow + Action: "events:PutEvents" + Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + + # SQS resource policy required to allow target on devops bus to send failed messages to target DLQ + # BlueServiceEventBusDlqPolicy: + # Type: AWS::SQS::QueuePolicy + # Properties: + # Queues: + # - !Ref BlueServiceEventBusDlqUrl + # PolicyDocument: + # Statement: + # - Action: + # - "SQS:SendMessage" + # Effect: "Allow" + # Resource: !Ref BlueServiceEventBusDlqArn + # Principal: + # Service: "events.amazonaws.com" + # Condition: + # ArnEquals: + # "aws:SourceArn": !GetAtt BlueServiceE2SubscriptionRule.Arn + +Outputs: + BlueE2Subscription: + Description: Rule ARN for blue service event 2 subscription + Value: !GetAtt PublicationApprovedSubscriptionRule.Arn diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 45f3158..0865f66 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -1,13 +1,24 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 Description: > Unicorn Properties Services - web interface. Add, list and get details for Unicorn Properties. -###################################### -# PARAMETERS -###################################### +#### METADATA +Metadata: + cfn-lint: + config: + ignore_checks: + - I3042 + - ES1001 + - ES1007 + - ES6000 + - WS2001 + +#### PARAMETERS Parameters: Stage: Type: String @@ -17,28 +28,7 @@ Parameters: - Dev - Prod -###################################### -# CONDITIONS -###################################### -Conditions: - IsProd: !Equals - - !Ref Stage - - Prod - -###################################### -# METADATA -###################################### -Metadata: - cfn-lint: - config: - ignore_checks: - - ES1007 - - ES6000 - - I3042 - -###################################### -# Mappings -###################################### +#### MAPPINGS Mappings: LogsRetentionPeriodMap: Local: @@ -47,42 +37,71 @@ Mappings: Days: 3 Prod: Days: 14 + Constants: + ServiceNamespace: + Value: "unicorn.web" + ProjectName: + Value: "AWS Serverless Developer Experience" -###################################### -# GLOBALS -# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst -###################################### +#### CONDITIONS +Conditions: + IsProd: !Equals [!Ref Stage, Prod] + +#### GLOBALS Globals: + Api: + OpenApiVersion: 3.0.1 Function: Runtime: python3.11 - Tracing: Active - Timeout: 15 MemorySize: 128 + Timeout: 15 + Tracing: Active Architectures: - - arm64 + - x86_64 Environment: Variables: DYNAMODB_TABLE: !Ref WebTable - EVENT_BUS: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" - SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" - POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" - POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default - POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default - POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" - POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + EVENT_BUS: !Ref UnicornWebEventBus + SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default + POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default + POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default + POWERTOOLS_METRICS_NAMESPACE: + !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage - project: AWS Serverless Developer Experience - service: Unicorn Web Service + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] -###################################### -# RESOURCES -###################################### +##### RESOURCES Resources: - ###################################### - # LAMBDA FUNCTIONS - ###################################### + #### SSM PARAMETERS + # Services own and share their namespace + UnicornWebNamespaceParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornWebNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + # Services share their event bus name and arn + UnicornWebEventBusParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornWebEventBus + Value: !GetAtt UnicornWebEventBus.Name + + UnicornWebEventBusArnParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornWebEventBusArn + Value: !GetAtt UnicornWebEventBus.Arn + + ##### LAMBDA FUNCTIONS SearchFunction: Type: AWS::Serverless::Function Properties: @@ -97,19 +116,33 @@ Resources: Properties: Path: /search/{country}/{city} Method: get - RestApiId: !Ref WebApi + RestApiId: + Ref: UnicornWebApi ListPropertiesByStreet: Type: Api Properties: Path: /search/{country}/{city}/{street} Method: get - RestApiId: !Ref WebApi + RestApiId: + Ref: UnicornWebApi PropertyDetails: Type: Api Properties: Path: /properties/{country}/{city}/{street}/{number} Method: get - RestApiId: !Ref WebApi + RestApiId: + Ref: UnicornWebApi + + SearchFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${SearchFunction}" + RetentionInDays: !FindInMap + - LogsRetentionPeriodMap + - !Ref Stage + - Days RequestApprovalFunction: Type: AWS::Serverless::Function @@ -118,18 +151,31 @@ Resources: Handler: approvals_service.request_approval_function.lambda_handler Policies: - EventBridgePutEventsPolicy: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" + EventBusName: !Ref UnicornWebEventBus - DynamoDBReadPolicy: TableName: !Ref WebTable - DynamoDBWritePolicy: TableName: !Ref WebTable Events: - AddProperty: - Type: Api + IngestQueue: + Type: SQS Properties: - Path: /request_approval - Method: post - RestApiId: !Ref WebApi + Queue: !GetAtt UnicornWebIngestQueue.Arn + BatchSize: 1 + Enabled: true + ScalingConfig: + MaximumConcurrency: 5 + + RequestApprovalFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${RequestApprovalFunction}" + RetentionInDays: !FindInMap + - LogsRetentionPeriodMap + - !Ref Stage + - Days PublicationApprovedFunction: Type: AWS::Serverless::Function @@ -137,48 +183,42 @@ Resources: CodeUri: src/ Handler: approvals_service.publication_approved_event_handler.lambda_handler Policies: + - DynamoDBReadPolicy: + TableName: !Ref WebTable - DynamoDBWritePolicy: TableName: !Ref WebTable Events: ApprovalEvent: Type: EventBridgeRule Properties: - RuleName: web.publication_approved-properties.pub_eval_completed - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" + RuleName: web.publicationapproved-properties.pubevalcompleted + EventBusName: !Ref UnicornWebEventBus Pattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + - !Ref UnicornWebEventBus detail-type: - PublicationEvaluationCompleted RetryPolicy: MaximumRetryAttempts: 5 MaximumEventAgeInSeconds: 900 DeadLetterConfig: - Arn: !GetAtt WebEventBusRuleDLQ.Arn + Arn: !GetAtt UnicornWebEventBusDLQ.Arn - ###################################### - # DLQs - ###################################### - WebEventBusRuleDLQ: - Type: AWS::SQS::Queue + PublicationApprovedFunctionLogGroup: + Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - Tags: - - Key: project - Value: AWS Serverless Developer Experience - - Key: service - Value: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" - - Key: stage - Value: !Ref Stage + LogGroupName: !Sub "/aws/lambda/${PublicationApprovedFunction}" + RetentionInDays: !FindInMap + - LogsRetentionPeriodMap + - !Ref Stage + - Days - ###################################### - # API - ###################################### - WebApi: + #### API GATEWAY REST API + UnicornWebApi: Type: AWS::Serverless::Api + DependsOn: UnicornWebApiGwAccountConfig Properties: StageName: !Ref Stage EndpointConfiguration: @@ -187,32 +227,125 @@ Resources: MethodSettings: - MetricsEnabled: true ResourcePath: /* - HttpMethod: '*' - LoggingLevel: !If + HttpMethod: "*" + LoggingLevel: !If - IsProd - ERROR - INFO ThrottlingBurstLimit: 10 ThrottlingRateLimit: 100 AccessLogSetting: - DestinationArn: !GetAtt WebApiLogGroup.Arn - Format: > - {"requestId":"$context.requestId", - "integration-error":"$context.integration.error", - "integration-status":"$context.integration.status", - "integration-latency":"$context.integration.latency", - "integration-requestId":"$context.integration.requestId", - "integration-integrationStatus":"$context.integration.integrationStatus", - "response-latency":"$context.responseLatency", - "status":"$context.status"} + DestinationArn: !GetAtt UnicornWebApiLogGroup.Arn + Format: !ToJsonString + requestId: $context.requestId + integration-error: $context.integration.error + integration-status: $context.integration.status + integration-latency: $context.integration.latency + integration-requestId: $context.integration.requestId + integration-integrationStatus: $context.integration.integrationStatus + response-latency: $context.responseLatency + status: $context.status + DefinitionBody: !Transform + Name: "AWS::Include" + Parameters: + Location: "api.yaml" Tags: stage: !Ref Stage - project: AWS Serverless Developer Experience - service: Unicorn Web Service + project: !FindInMap [Constants, ProjectName, Value] + namespace: Unicorn Web Service + + UnicornWebApiGwAccountConfig: + Type: AWS::ApiGateway::Account + Properties: + CloudWatchRoleArn: !GetAtt UnicornWebApiGwAccountConfigRole.Arn + + # API GW IAM roles + UnicornWebApiGwAccountConfigRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - apigateway.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + + UnicornWebApiIntegrationRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + Effect: Allow + Action: sts:AssumeRole + Principal: + Service: apigateway.amazonaws.com + Policies: + - PolicyName: AllowSqsIntegration + PolicyDocument: + Statement: + - Effect: Allow + Action: + - sqs:SendMessage + - sqs:GetQueueUrl + Resource: !GetAtt UnicornWebIngestQueue.Arn + - PolicyName: AllowLambdaInvocation + PolicyDocument: + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !GetAtt SearchFunction.Arn + + # API GW Cloudwatch Log Group + UnicornWebApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap + - LogsRetentionPeriodMap + - !Ref Stage + - Days + + #### INGEST QUEUES + UnicornWebIngestQueue: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + RedrivePolicy: + deadLetterTargetArn: !GetAtt UnicornWebIngestDLQ.Arn + maxReceiveCount: 1 + VisibilityTimeout: 20 + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - ###################################### - # DYNAMODB - ###################################### + UnicornWebIngestDLQ: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + Tags: + - Key: stage + Value: !Ref Stage + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + ##### DYNAMODB WebTable: Type: AWS::DynamoDB::Table UpdateReplacePolicy: Delete @@ -229,88 +362,182 @@ Resources: - AttributeName: "SK" KeyType: "RANGE" BillingMode: PAY_PER_REQUEST + Tags: + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + - Key: stage + Value: !Ref Stage - ###################################### - # CLOUDWATCH LOG GROUPS - ###################################### - WebApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete + #### EVENT BUS + # Event bus for Properties Web used to publish and consume events + UnicornWebEventBus: + Type: AWS::Events::EventBus Properties: - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + Name: !Sub UnicornWebBus-${Stage} - SearchFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete + # Event bus policy to restrict who can publish events (should only be services from WebServiceNamespace) + UnicornWebEventBusPublishPolicy: + Type: AWS::Events::EventBusPolicy Properties: - LogGroupName: !Sub "/aws/lambda/${SearchFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + EventBusName: !Ref UnicornWebEventBus + StatementId: !Sub WebPublishEventsPolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: events:PutEvents + Resource: !GetAtt UnicornWebEventBus.Arn + Condition: + StringEquals: + events:source: + - !FindInMap [Constants, ServiceNamespace, Value] - RequestApprovalFunctionLogGroup: + # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. + # Update this policy as you get new subscribers by adding their namespace to events:source + # PropertiesServiceRulePolicy: + # Type: "AWS::Events::EventBusPolicy" + # Properties: + # EventBusName: !Ref UnicornWebEventBus + # StatementId: !Sub CreateRulePolicy-${Stage} + # Statement: + # Effect: Allow + # Principal: + # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + # Action: + # - "events:PutRule" + # - "events:DeleteRule" + # - "events:DescribeRule" + # - "events:DisableRule" + # - "events:EnableRule" + # - "events:PutTargets" + # - "events:RemoveTargets" + # Resource: + # - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornWebEventBus.Name}/* + # Condition: + # StringEqualsIfExists: + # "events:creatorAccount": "${aws:PrincipalAccount}" + # StringEquals: + # "events:source": + # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + # "Null": + # "events:source": "false" + + # Catchall rule used for development purposes. + UnicornWebCatchAllRule: + Type: AWS::Events::Rule + Metadata: + cfn-lint: + config: + ignore_checks: + - ES4000 + Properties: + Name: contracts.catchall + Description: Catch all events published by the web service. + EventBusName: !Ref UnicornWebEventBus + EventPattern: + source: + - prefix: "" + State: ENABLED #You may want to disable this rule in production + Targets: + - Arn: !GetAtt UnicornWebCatchAllLogGroup.Arn + Id: !Sub UnicornContractsCatchAllLogGroupTarget-${Stage} + + # CloudWatch log group used to catch all events + UnicornWebCatchAllLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - LogGroupName: !Sub "/aws/lambda/${RequestApprovalFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: 3 - PublicationApprovedFunctionLogGroup: - Type: AWS::Logs::LogGroup + # Permissions to allow EventBridge to send logs to CloudWatch + EventBridgeCloudWatchLogGroupPolicy: + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub UnicornWeb-EventBridgeToCWLogsPolicy-${Stage} + # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "delivery.logs.amazonaws.com", + "events.amazonaws.com" + ] + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "${UnicornWebCatchAllLogGroup.Arn}" + ] + } + ] + } + + ##### DLQs + UnicornWebEventBusDLQ: + Type: AWS::SQS::Queue UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - LogGroupName: !Sub "/aws/lambda/${PublicationApprovedFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + Tags: + - Key: project + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + - Key: stage + Value: !Ref Stage -###################################### -# OUTPUTS -###################################### +#### OUTPUTS Outputs: - SearchPropertiesByCity: - Description: "GET request to list all properties in a given city" - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}" - - SearchPropertiesByStreet: - Description: "GET request to list all properties in a given street" - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}/{street}" - - PropertyApproval: - Description: "POST request to add a property to the database" - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/request_approval" - - PropertyDetails: - Description: "GET request to get the full details of a single property" - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/properties/{country}/{city}/{street}/{number}" + BaseUrl: + Description: Web service API endpoint + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" + ApiUrl: + Description: Web service API endpoint + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" - IsProd: - Description: Is Production? - Value: !If - - IsProd - - "true" - - "false" + IngestQueueUrl: + Description: URL for the Ingest SQS Queue + Value: !GetAtt UnicornWebIngestQueue.QueueUrl WebTableName: Description: Name of the DynamoDB Table for Unicorn Web Value: !Ref WebTable - ApiUrl: - Description: Web service API endpoint - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + SearchFunctionArn: + Description: Search function ARN + Value: !GetAtt SearchFunction.Arn + RequestApprovalFunctionArn: + Description: Approval function ARN + Value: !GetAtt RequestApprovalFunction.Arn + PublicationApprovedFunctionArn: + Description: Publication evaluation event handler function ARN + Value: !GetAtt PublicationApprovedFunction.Arn - BaseUrl: - Description: Web service API endpoint - Value: !Sub "https://${WebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" + IsProd: + Description: Is Production? + Value: !If [IsProd, "true", "false"] + + ApiSearchPropertiesByCity: + Description: "GET request to list all properties in a given city" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}" + ApiSearchPropertiesByStreet: + Description: "GET request to list all properties in a given street" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}/{street}" + ApiPropertyApproval: + Description: "POST request to add a property to the database" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/request_approval" + ApiPropertyDetails: + Description: "GET request to get the full details of a single property" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/properties/{country}/{city}/{street}/{number}" From 94884faa177d68a16fd88953047ec22ef95126fc Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 29 Aug 2023 16:00:57 +1000 Subject: [PATCH 09/87] chore: Implemented final Unit tests for unicorn_contracts --- .../contract_event_handler.py | 2 +- unicorn_contracts/template.yaml | 18 +-- .../integration/test_create_contract_apigw.py | 3 +- .../integration/test_update_contract_apigw.py | 3 +- .../transformations/ddb_contract.jq | 2 +- .../pipes/pipes_payloads/create_contract.json | 2 +- .../pipes/pipes_payloads/update_contract.json | 4 +- .../streams_payloads/create_contract.json | 2 +- .../streams_payloads/delete_contract.json | 2 +- .../streams_payloads/update_contract.json | 4 +- .../unit/{events.py => event_generator.py} | 71 ++++------ .../create_contract_invalid_1.json | 0 .../create_contract_valid_1.json | 0 .../update_contract_valid_1.json | 0 unicorn_contracts/tests/unit/helper.py | 4 +- .../tests/unit/test_contract_event_handler.py | 80 ++++------- .../unit/test_create_contract_function.py | 123 ----------------- .../unit/test_update_contract_function.py | 128 ------------------ 18 files changed, 77 insertions(+), 371 deletions(-) rename unicorn_contracts/tests/unit/{events.py => event_generator.py} (75%) rename unicorn_contracts/tests/unit/{event_payloads => events}/create_contract_invalid_1.json (100%) rename unicorn_contracts/tests/unit/{event_payloads => events}/create_contract_valid_1.json (100%) rename unicorn_contracts/tests/unit/{event_payloads => events}/update_contract_valid_1.json (100%) delete mode 100644 unicorn_contracts/tests/unit/test_create_contract_function.py delete mode 100644 unicorn_contracts/tests/unit/test_update_contract_function.py diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 674d368..31bf3fb 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -81,7 +81,7 @@ def create_contract(event: dict) -> None: "property_id": event["property_id"], # PK "address": event["address"], "seller_name": event["seller_name"], - "contact_created": current_date, + "contract_created": current_date, "contract_last_modified_on": current_date, "contract_id": str(uuid.uuid4()), "contract_status": ContractStatus.DRAFT.name, diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 38e38eb..3ba33d5 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -113,15 +113,15 @@ Resources: TableName: !Ref ContractsTable - DynamoDBReadPolicy: TableName: !Ref ContractsTable - Events: - IngestQueue: - Type: SQS - Properties: - Queue: !GetAtt UnicornContractsIngestQueue.Arn - BatchSize: 1 - Enabled: true - ScalingConfig: - MaximumConcurrency: 5 + # Events: + # IngestQueue: + # Type: SQS + # Properties: + # Queue: !GetAtt UnicornContractsIngestQueue.Arn + # BatchSize: 1 + # Enabled: true + # ScalingConfig: + # MaximumConcurrency: 5 ContractEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup diff --git a/unicorn_contracts/tests/integration/test_create_contract_apigw.py b/unicorn_contracts/tests/integration/test_create_contract_apigw.py index a87c920..85fbd18 100644 --- a/unicorn_contracts/tests/integration/test_create_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_create_contract_apigw.py @@ -1,7 +1,8 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -from unittest import TestCase + import requests +from unittest import TestCase from . import get_stack_output diff --git a/unicorn_contracts/tests/integration/test_update_contract_apigw.py b/unicorn_contracts/tests/integration/test_update_contract_apigw.py index 72af113..a466bb3 100644 --- a/unicorn_contracts/tests/integration/test_update_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_update_contract_apigw.py @@ -1,7 +1,8 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -from unittest import TestCase + import requests +from unittest import TestCase from . import get_stack_output diff --git a/unicorn_contracts/tests/integration/transformations/ddb_contract.jq b/unicorn_contracts/tests/integration/transformations/ddb_contract.jq index 5f11110..54e3bf8 100644 --- a/unicorn_contracts/tests/integration/transformations/ddb_contract.jq +++ b/unicorn_contracts/tests/integration/transformations/ddb_contract.jq @@ -9,6 +9,6 @@ street: .address.M.street.S, }, contract_status: .contract_status.S, - contact_created: .contact_created.S, + contract_created: .contract_created.S, contract_last_modified_on: .contract_last_modified_on.S } | del(..|nulls) diff --git a/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json b/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json index 0110b72..14dbec8 100644 --- a/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json +++ b/unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json @@ -34,7 +34,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { diff --git a/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json b/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json index e6f8788..5fc12de 100644 --- a/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json +++ b/unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json @@ -34,7 +34,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { @@ -73,7 +73,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { diff --git a/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json index 856608b..cf2f17d 100644 --- a/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json +++ b/unicorn_contracts/tests/pipes/streams_payloads/create_contract.json @@ -28,7 +28,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { diff --git a/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json index bf8b0e6..71636ea 100644 --- a/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json +++ b/unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json @@ -28,7 +28,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { diff --git a/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json b/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json index 44bb357..2bc30af 100644 --- a/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json +++ b/unicorn_contracts/tests/pipes/streams_payloads/update_contract.json @@ -28,7 +28,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { @@ -67,7 +67,7 @@ "seller_name": { "S": "John Smith" }, - "contact_created": { + "contract_created": { "S": "25/08/2023 02:14:20" }, "contract_id": { diff --git a/unicorn_contracts/tests/unit/events.py b/unicorn_contracts/tests/unit/event_generator.py similarity index 75% rename from unicorn_contracts/tests/unit/events.py rename to unicorn_contracts/tests/unit/event_generator.py index 25df2b2..5601a21 100644 --- a/unicorn_contracts/tests/unit/events.py +++ b/unicorn_contracts/tests/unit/event_generator.py @@ -122,6 +122,15 @@ def sqs_event(messages: List[dict], md5ofbody = hashlib.md5(body.encode('utf-8')).hexdigest() rcv_timestamp = int(time.time() + (random.randint(0, 500)/1000)) # Random delay of 0-500ms + msg_attributes = dict() + for attr, val in message.get('attributes', dict()).items(): + msg_attributes[attr] = { + "dataType": "String", + "stringValue": val, + "stringListValues": [], + "binaryListValues": [], + } + records.append({ "messageId": str(uuid.uuid4()), "receiptHandle": "MessageReceiptHandle", @@ -131,8 +140,9 @@ def sqs_event(messages: List[dict], "SentTimestamp": f"{int(time.time())}", "SenderId": f"{account_id}", "ApproximateFirstReceiveTimestamp": str(rcv_timestamp), + "AWSTraceHeader": "Root=1-64ed8007-277749e74aefce547c22fb79;Parent=11d035ab3958d16e;Sampled=1", }, - "messageAttributes": message.get('attributes', dict()), + "messageAttributes": msg_attributes, "md5OfBody": md5ofbody, "eventSource": "aws:sqs", "eventSourceARN": f"arn:aws:sqs:{aws_region}:{account_id}:{queue_name}", @@ -143,48 +153,23 @@ def sqs_event(messages: List[dict], -# def sqs_event(messages: List[dict], -# queue_name: str = 'MyQueue', -# account_id: int = 0, -# aws_region: str = 'us-east-1' -# ) -> dict: -# try: -# return { -# "Records": [ -# sqs_record(m.get('body'), m.get('attributes', dict()), -# queue_name=queue_name, -# account_id=account_id, -# aws_region=aws_region) -# for m in messages -# ] -# } -# except KeyError as e: -# raise Exception(f'Each message should contain at least a `body` key: {messages}') from e - -# def sqs_record(body: Any, -# attributes: dict[str,Any] = dict(), -# queue_name: str = 'MyQueue', -# account_id: int = 0, -# aws_region: str = 'us-east-1' -# ) -> dict: -# body = json.dumps(body) -# if account_id == 0: -# account_id = random.randint(100000000000,999999999999) - -# return { -# "messageId": str(uuid.uuid4()), -# "receiptHandle": "MessageReceiptHandle", -# "body": body, -# "attributes": { -# "ApproximateReceiveCount": "1", -# "SentTimestamp": int(time.time()), -# "SenderId": f"{account_id}", -# "ApproximateFirstReceiveTimestamp": str(int(time.time() + (random.randint(0, 500)/1000))), # Random delay of 0-500ms +# { +# "Records": [ +# { +# "messageAttributes": { +# "HttpMethod": { +# "stringValue": "PUT", +# "stringListValues": [], +# "binaryListValues": [], +# "dataType": "String" +# } # }, -# "messageAttributes": attributes, -# "md5OfBody": hashlib.md5(body).hexdigest(), +# "md5OfMessageAttributes": "39c36267fdf9c8d354b1069e44662d24", +# "md5OfBody": "4bc398b7ae8e52d7f7eaed3cb76c12ef", # "eventSource": "aws:sqs", -# "eventSourceARN": f"arn:aws:sqs:{aws_region}:{account_id}:{queue_name}", -# "awsRegion": aws_region, -# } +# "eventSourceARN": "arn:aws:sqs:ap-southeast-2:718758479978:uni-prop-local-contract-UnicornContractsIngestQueue-p5zisiK0Xbxn", +# "awsRegion": "ap-southeast-2" +# } +# ] +# } \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json b/unicorn_contracts/tests/unit/events/create_contract_invalid_1.json similarity index 100% rename from unicorn_contracts/tests/unit/event_payloads/create_contract_invalid_1.json rename to unicorn_contracts/tests/unit/events/create_contract_invalid_1.json diff --git a/unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json b/unicorn_contracts/tests/unit/events/create_contract_valid_1.json similarity index 100% rename from unicorn_contracts/tests/unit/event_payloads/create_contract_valid_1.json rename to unicorn_contracts/tests/unit/events/create_contract_valid_1.json diff --git a/unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json b/unicorn_contracts/tests/unit/events/update_contract_valid_1.json similarity index 100% rename from unicorn_contracts/tests/unit/event_payloads/update_contract_valid_1.json rename to unicorn_contracts/tests/unit/events/update_contract_valid_1.json diff --git a/unicorn_contracts/tests/unit/helper.py b/unicorn_contracts/tests/unit/helper.py index eb10e8f..a1f748a 100644 --- a/unicorn_contracts/tests/unit/helper.py +++ b/unicorn_contracts/tests/unit/helper.py @@ -7,7 +7,7 @@ TABLE_NAME = 'table1' EVENTBUS_NAME = 'test-eventbridge' SQS_QUEUE_NAME = 'test_sqs' -EVENTS_DIR = Path(__file__).parent / 'event_payloads' +EVENTS_DIR = Path(__file__).parent / 'events' def load_event(filename): @@ -83,7 +83,7 @@ def create_ddb_table_contracts_with_entry(dynamodb): table.meta.client.get_waiter('table_exists').wait(TableName=TABLE_NAME) contract = { "property_id": "usa/anytown/main-street/123", # PK - "contact_created": "01/08/2022 20:36:30", + "contract_created": "01/08/2022 20:36:30", "contract_last_modified_on": "01/08/2022 20:36:30", "contract_id": "11111111", "address": { diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py index 3b179d0..3deab43 100644 --- a/unicorn_contracts/tests/unit/test_contract_event_handler.py +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -5,12 +5,13 @@ from importlib import reload import pytest -from unittest import mock, TestCase +from unittest import mock from botocore.exceptions import ClientError -from .events import sqs_event -from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_test_sqs_ingestion_queue +from .event_generator import sqs_event from .helper import TABLE_NAME +from .helper import load_event, return_env_vars_dict +from .helper import create_ddb_table_contracts, create_test_sqs_ingestion_queue, create_ddb_table_contracts_with_entry @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) @@ -30,66 +31,35 @@ def test_valid_create_event(dynamodb, sqs, lambda_context): res = dynamodb.Table(TABLE_NAME).get_item(Key={'property_id': payload['property_id']}) - TestCase().assertDictEqual(res, res | {"Item": payload}) + assert res['Item']['property_id'] == payload['property_id'] + assert res['Item']['contract_status'] == 'DRAFT' + assert res['Item']['seller_name'] == payload['seller_name'] + assert res['Item']['address']['country'] == payload['address']['country'] + assert res['Item']['address']['city'] == payload['address']['city'] + assert res['Item']['address']['street'] == payload['address']['street'] + assert res['Item']['address']['number'] == payload['address']['number'] - # Read value in DDB - # data = json.loads(ret["body"]) - # assert ret["statusCode"] == 200 - # assert "property_id" in data.keys() - # assert "contract_status" in data.keys() - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_body_event(dynamodb, eventbridge, mocker, lambda_context): -# event = load_event('events/create_missing_body_event.json') - -# from contracts_service import contract_event_handler # noqa: F401 -# reload(contract_event_handler) +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_valid_update_event(dynamodb, sqs, lambda_context): + payload = load_event('update_contract_valid_1') + event = sqs_event([{'body': payload, 'attributes': {'HttpMethod': 'PUT'}}]) -# create_ddb_table_contracts(dynamodb) - -# contract_event_handler.lambda_handler(event, lambda_context) - -# # Read value in DDB -# # data = json.loads(ret["body"]) - -# # assert ret["statusCode"] == 200 -# # assert "property_id" in data.keys() -# # assert "contract_status" in data.keys() - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_empty_dict_body_event(dynamodb, eventbridge, mocker, lambda_context): -# apigw_event = load_event('events/create_empty_dict_body_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) - -# ret = create_contract_function.lambda_handler(apigw_event, lambda_context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - + # Loading function here so that mocking works correctly. + from contracts_service import contract_event_handler # noqa: F401 + # Reload is required to prevent function setup reuse from another test + reload(contract_event_handler) -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_wrong_event_data(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_wrong_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) + create_ddb_table_contracts_with_entry(dynamodb) + create_test_sqs_ingestion_queue(sqs) -# context = LambdaContext() + contract_event_handler.lambda_handler(event, lambda_context) -# ret = create_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) + res = dynamodb.Table(TABLE_NAME).get_item(Key={'property_id': payload['property_id']}) -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." + assert res['Item']['property_id'] == payload['property_id'] + assert res['Item']['contract_status'] == 'APPROVED' @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) diff --git a/unicorn_contracts/tests/unit/test_create_contract_function.py b/unicorn_contracts/tests/unit/test_create_contract_function.py deleted file mode 100644 index ae3552f..0000000 --- a/unicorn_contracts/tests/unit/test_create_contract_function.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -# import os -# import json -# from importlib import reload - -# import pytest -# from unittest import mock -# from botocore.exceptions import ClientError - -# from .helper import LambdaContext, load_event, return_env_vars_dict, create_ddb_table_contracts, create_test_eventbridge_bus - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_valid_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_valid_event.json') - -# # Loading function here so that mocking works correctly. -# from contracts_service import contract_event_handler # noqa: F401 - -# # Reload is required to prevent function setup reuse from another test -# reload(create_contract_function) - -# create_ddb_table_contracts(dynamodb) -# create_test_eventbridge_bus(eventbridge) - -# context = LambdaContext() -# ret = create_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 200 -# assert "property_id" in data.keys() -# assert "contract_status" in data.keys() - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_body_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_missing_body_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() -# ret = create_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_empty_dict_body_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_empty_dict_body_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() - -# ret = create_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_wrong_event_data(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_wrong_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() - -# ret = create_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): -# del os.environ['DYNAMODB_TABLE'] -# # Loading function here so that mocking works correctly -# from contracts_service import contract_event_handler # noqa: F401 -# with pytest.raises(EnvironmentError): -# reload(create_contract_function) - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_eb_env_var(dynamodb, eventbridge, mocker): -# del os.environ['EVENT_BUS'] -# # Loading function here so that mocking works correctly -# from contracts_service import helper -# with pytest.raises(EnvironmentError): -# reload(helper) - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_sm_env_var(dynamodb, eventbridge, mocker): -# del os.environ['SERVICE_NAMESPACE'] -# # Loading function here so that mocking works correctly -# from contracts_service import helper -# with pytest.raises(EnvironmentError): -# reload(helper) - - -# @mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) -# def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/create_valid_event.json') -# from contracts_service import contract_event_handler # noqa: F401 -# reload(create_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() -# with pytest.raises(ClientError): -# create_contract_function.lambda_handler(apigw_event, context) diff --git a/unicorn_contracts/tests/unit/test_update_contract_function.py b/unicorn_contracts/tests/unit/test_update_contract_function.py deleted file mode 100644 index 609d9ea..0000000 --- a/unicorn_contracts/tests/unit/test_update_contract_function.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -# import os -# import json -# from importlib import reload - -# import pytest -# from unittest import mock -# # from moto import mock_dynamodb, mock_events -# # from botocore.exceptions import ClientError - -# # from contracts_service.exceptions import EventValidationException - -# from .lambda_context import LambdaContext -# from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts, create_ddb_table_contracts_with_entry, create_test_eventbridge_bus - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_valid_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/update_valid_event.json') -# # Loading function here so that mocking works correctly -# from contracts_service import update_contract_function -# reload(update_contract_function) - -# create_ddb_table_contracts_with_entry(dynamodb) -# create_test_eventbridge_bus(eventbridge) - -# context = LambdaContext() -# ret = update_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 200 -# assert "contract_status" in data.keys() -# assert "property_id" in data.keys() - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_body_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/update_missing_body_event.json') -# from contracts_service import update_contract_function -# reload(update_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() -# ret = update_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_empty_dict_body_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/update_empty_dict_body_event.json') -# from contracts_service import update_contract_function -# reload(update_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() - -# ret = update_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_wrong_event_data(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/update_wrong_event.json') -# from contracts_service import update_contract_function -# reload(update_contract_function) -# create_ddb_table_contracts(dynamodb) - -# context = LambdaContext() - -# ret = update_contract_function.lambda_handler(apigw_event, context) -# data = json.loads(ret["body"]) - -# assert ret["statusCode"] == 400 -# assert "message" in ret["body"] -# assert data["message"] == "Event body not valid." - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_ddb_env_var(dynamodb, eventbridge, mocker): -# del os.environ['DYNAMODB_TABLE'] -# load_event('events/update_valid_event.json') -# # Loading function here so that mocking works correctly -# from contracts_service import update_contract_function -# with pytest.raises(EnvironmentError): -# reload(update_contract_function) - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_eb_env_var(dynamodb, eventbridge, mocker): -# del os.environ['EVENT_BUS'] -# load_event('events/update_valid_event.json') -# # Loading function here so that mocking works correctly -# from contracts_service import helper -# with pytest.raises(EnvironmentError): -# reload(helper) - - -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_missing_sm_env_var(dynamodb, eventbridge, mocker): -# del os.environ['SERVICE_NAMESPACE'] -# load_event('events/update_valid_event.json') -# # Loading function here so that mocking works correctly -# from contracts_service import helper -# with pytest.raises(EnvironmentError): -# reload(helper) - - -# @mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) -# def test_wrong_dynamodb_table(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/update_valid_event.json') -# from contracts_service import update_contract_function -# reload(update_contract_function) -# create_ddb_table_contracts_with_entry(dynamodb) - -# context = LambdaContext() -# # with pytest.raises(ClientError): -# ret = update_contract_function.lambda_handler(apigw_event, context) -# assert ret["statusCode"] == 400 From 7ee33b88020fb4736aee27447fa1ed5ed8119fdc Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 31 Aug 2023 14:32:03 +1000 Subject: [PATCH 10/87] test: Fixed all unit, integration and Curl tests for unicorn_contracts --- unicorn_contracts/Makefile | 8 +- unicorn_contracts/poetry.lock | 42 ++++++---- unicorn_contracts/pyproject.toml | 1 + unicorn_contracts/samconfig.yaml | 31 ++++++-- unicorn_contracts/subscriber-policies.yaml | 47 +++++++++++ unicorn_contracts/template.yaml | 52 ++++++------- .../tests/integration/__init__.py | 51 ++++++++++-- unicorn_contracts/tests/integration/cwlogs.py | 78 +++++++++++++++++++ .../integration/test_create_contract_apigw.py | 56 ++++++++----- .../integration/test_update_contract_apigw.py | 57 ++++++++++---- 10 files changed, 322 insertions(+), 101 deletions(-) create mode 100644 unicorn_contracts/subscriber-policies.yaml create mode 100644 unicorn_contracts/tests/integration/cwlogs.py diff --git a/unicorn_contracts/Makefile b/unicorn_contracts/Makefile index 0bd1698..224aeba 100644 --- a/unicorn_contracts/Makefile +++ b/unicorn_contracts/Makefile @@ -11,7 +11,7 @@ ddbPropertyId = $(call get_ddb_key,create_contract_valid_payload_1) ci: clean build deploy build: - # sam validate --lint + sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt sam build -c $(DOCKER_OPTS) @@ -30,7 +30,7 @@ ci_init: #### Tests -test: unit-test +test: unit-test integration-test curl-test unit-test: poetry run pytest tests/unit/ @@ -56,10 +56,6 @@ curl-test: clean-tests @echo "[DONE]" -x: - @echo "apiUrl: $(apiUrl)" - @echo "ddbPropertyId: $(ddbPropertyId)" - @echo "$(call get_ddb_key,create_contract_valid_payload_1)" clean-tests: $(call ddb_delete,$(ddbPropertyId)) || true diff --git a/unicorn_contracts/poetry.lock b/unicorn_contracts/poetry.lock index ff73313..12c488c 100644 --- a/unicorn_contracts/poetry.lock +++ b/unicorn_contracts/poetry.lock @@ -1,14 +1,25 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "arnparse" +version = "0.0.2" +description = "Parse ARNs using Python" +optional = false +python-versions = "*" +files = [ + {file = "arnparse-0.0.2-py2.py3-none-any.whl", hash = "sha256:b0906734e4b8f19e39b1e32944c6cd6274b6da90c066a83882ac7a11d27553e0"}, + {file = "arnparse-0.0.2.tar.gz", hash = "sha256:cb87f17200d07121108a9085d4a09cc69a55582647776b9a917b0b1f279db8f8"}, +] + [[package]] name = "aws-lambda-powertools" -version = "2.23.0" +version = "2.23.1" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, - {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, + {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, + {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, ] [package.dependencies] @@ -18,6 +29,7 @@ typing-extensions = ">=4.6.2,<5.0.0" [package.extras] all = ["aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "pydantic (>=1.8.2,<2.0.0)"] aws-sdk = ["boto3 (>=1.20.32,<2.0.0)"] +datadog = ["datadog-lambda (>=4.77.0,<5.0.0)"] parser = ["pydantic (>=1.8.2,<2.0.0)"] tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] @@ -39,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.33" +version = "1.28.37" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.33-py3-none-any.whl", hash = "sha256:07997e299e7b87afbbb25dc9de677017eafbd96b4f1b81e931d5127716dc6dd1"}, - {file = "boto3-1.28.33.tar.gz", hash = "sha256:fafc0eda7ebe7878be2ab934558ea1776cbd1bd624ce9e9b827e304d301ccd00"}, + {file = "boto3-1.28.37-py3-none-any.whl", hash = "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6"}, + {file = "boto3-1.28.37.tar.gz", hash = "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2"}, ] [package.dependencies] -botocore = ">=1.31.33,<1.32.0" +botocore = ">=1.31.37,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.33" +version = "1.31.37" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.33-py3-none-any.whl", hash = "sha256:1b76549c45f712ca9734888e60a2ab9c857e6e6025b156b36c344162a7e9d0dc"}, - {file = "botocore-1.31.33.tar.gz", hash = "sha256:3fd7cb89cf834b28bc7e8427cb29bb861b10652a3bebe9d0d18d9a2c1e4f3f67"}, + {file = "botocore-1.31.37-py3-none-any.whl", hash = "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06"}, + {file = "botocore-1.31.37.tar.gz", hash = "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de"}, ] [package.dependencies] @@ -491,13 +503,13 @@ files = [ [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -852,4 +864,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "175a485c562e962bc87f676afaab46d70858d5fa79a2650f9c3b6fa429ce683a" +content-hash = "d98f5abd0cb97c5ab987c900089d8161e28d85067d8e3f04ba17adb06b14d871" diff --git a/unicorn_contracts/pyproject.toml b/unicorn_contracts/pyproject.toml index 483d349..c25be68 100644 --- a/unicorn_contracts/pyproject.toml +++ b/unicorn_contracts/pyproject.toml @@ -22,6 +22,7 @@ requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" pyyaml = "^6.0.1" +arnparse = "^0.0.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 3ac9ade..015b728 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -1,9 +1,10 @@ version: 0.1 + default: global: parameters: - stack_name: "uni-prop-local-contract" - s3_prefix: "uni-prop-local-contract" + stack_name: uni-prop-local-contract + s3_prefix: uni-prop-local-contract resolve_s3: true resolve_image_repositories: true build: @@ -12,14 +13,12 @@ default: parallel: true deploy: parameters: - disable_rollback: false + disable_rollback: true confirm_changeset: false fail_on_empty_changeset: false - capabilities: "CAPABILITY_IAM" + capabilities: CAPABILITY_IAM parameter_overrides: - "Stage=Local" - package: - parameters: {} validate: parameters: lint: true @@ -28,7 +27,23 @@ default: watch: true local_start_api: parameters: - warm_containers: "EAGER" + warm_containers: EAGER local_start_lambda: parameters: - warm_containers: "EAGER" + warm_containers: EAGER + +subscribers: + global: + parameters: + stack_name: uni-prop-local-contract-subscribers + s3_prefix: uni-prop-local-contract-subscribers + resolve_s3: true + deploy: + parameters: + template_file: subscriber-policies.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" diff --git a/unicorn_contracts/subscriber-policies.yaml b/unicorn_contracts/subscriber-policies.yaml new file mode 100644 index 0000000..ea3daf6 --- /dev/null +++ b/unicorn_contracts/subscriber-policies.yaml @@ -0,0 +1,47 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: "2010-09-09" +Description: + Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by the Contracts Service. + +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +Resources: + # Update this policy as you get new subscribers by adding their namespace to events:source + CrossServiceCreateRulePolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBus}}" + StatementId: !Sub CreateRulePolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - events:PutRule + - events:DeleteRule + - events:DescribeRule + - events:DisableRule + - events:EnableRule + - events:PutTargets + - events:RemoveTargets + Resource: + - !Sub + - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* + - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBus}}" + Condition: + StringEqualsIfExists: + "events:creatorAccount": "${aws:PrincipalAccount}" + StringEquals: + "events:source": + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + "Null": + "events:source": "false" diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 3ba33d5..e8ce3f7 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -12,8 +12,8 @@ Metadata: cfn-lint: config: ignore_checks: - - I3042 - - ES1007 + - ES4000 + - ES6000 - ES1001 - WS2001 @@ -61,6 +61,7 @@ Globals: Variables: DYNAMODB_TABLE: !Ref ContractsTable SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_LOGGER_CASE: PascalCase POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default @@ -113,15 +114,15 @@ Resources: TableName: !Ref ContractsTable - DynamoDBReadPolicy: TableName: !Ref ContractsTable - # Events: - # IngestQueue: - # Type: SQS - # Properties: - # Queue: !GetAtt UnicornContractsIngestQueue.Arn - # BatchSize: 1 - # Enabled: true - # ScalingConfig: - # MaximumConcurrency: 5 + Events: + IngestQueue: + Type: SQS + Properties: + Queue: !GetAtt UnicornContractsIngestQueue.Arn + BatchSize: 1 + Enabled: true + ScalingConfig: + MaximumConcurrency: 5 ContractEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -206,10 +207,9 @@ Resources: AssumeRolePolicyDocument: Statement: - Effect: Allow - Principal: - Service: - - apigateway.amazonaws.com Action: sts:AssumeRole + Principal: + Service: apigateway.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs @@ -221,6 +221,7 @@ Resources: Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub UnicornContractsIngestQueue-${Stage} RedrivePolicy: deadLetterTargetArn: !GetAtt UnicornContractsIngestDLQ.Arn maxReceiveCount: 1 @@ -237,14 +238,10 @@ Resources: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete DeletionPolicy: Delete - Metadata: - cfn-lint: - config: - ignore_checks: - - ES6000 Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub UnicornContractsIngestDLQ-${Stage} Tags: - Key: stage Value: !Ref Stage @@ -269,12 +266,12 @@ Resources: StreamViewType: NEW_AND_OLD_IMAGES BillingMode: PAY_PER_REQUEST Tags: + - Key: stage + Value: !Ref Stage - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !FindInMap [Constants, ServiceNamespace, Value] - - Key: stage - Value: !Ref Stage #### EVENT BUS # Event bus for Contract Service used to publish and consume events @@ -294,8 +291,7 @@ Resources: Principal: AWS: - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - Action: - - events:PutEvents + Action: events:PutEvents Resource: !GetAtt UnicornContractsEventBus.Arn Condition: StringEquals: @@ -305,11 +301,6 @@ Resources: # Catchall rule used for development purposes. UnicornContractsCatchAllRule: Type: AWS::Events::Rule - Metadata: - cfn-lint: - config: - ignore_checks: - - ES4000 Properties: Name: contracts.catchall Description: Catch all events published by the contracts service. @@ -439,10 +430,13 @@ Outputs: Description: DynamoDB table storing contract information Value: !Ref ContractsTable - ContractEventHandlerFunctionArn: + ContractEventHandlerFunction: Description: Create Contract function ARN Value: !GetAtt ContractEventHandlerFunction.Arn + UnicornContractsCatchAllLogGroupArn: + Value: !GetAtt UnicornContractsCatchAllLogGroup.Arn + IsProd: Description: Is Production? Value: !If [IsProd, "true", "false"] diff --git a/unicorn_contracts/tests/integration/__init__.py b/unicorn_contracts/tests/integration/__init__.py index 1cc6bfd..006dbae 100644 --- a/unicorn_contracts/tests/integration/__init__.py +++ b/unicorn_contracts/tests/integration/__init__.py @@ -1,19 +1,23 @@ -from pathlib import Path +from typing import Iterator -from yaml import load -# From: https://pyyaml.org/wiki/PyYAMLDocumentation -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader +import json +from pathlib import Path import boto3 +from arnparse import arnparse +from yaml import load, Loader +#### CONSTANTS DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' STACK_OUTPUTS = dict() +EVENTS_DIR = Path(__file__).parent / 'events' -cfn = boto3.client("cloudformation") + +#### AWS SDK Objects +cfn = boto3.client('cloudformation') +cwl = boto3.client('logs') +ddb = boto3.client('dynamodb') def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: @@ -42,3 +46,34 @@ def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> st return outputs[output_name] except KeyError as e: raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e + + +def get_event_payload(file) -> dict: + return json.load(open(EVENTS_DIR / f'{file}.json', 'r')) + + +def override_payload_number(p: dict, number: int) -> dict: + p['address']['number'] = number + a = p["address"] + p['property_id'] = f'{a["country"]}/{a["city"]}/{a["street"]}/{a["number"]}'.replace(' ', '-').lower() + return p + + +def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: + group_name = arnparse(eb_log_group_arn).resource + + # Get the CW LogStream with the latest log messages + stream_response = cwl.describe_log_streams(logGroupName=group_name, orderBy='LastEventTime',descending=True,limit=3) + latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] + # Fetch log events from that stream + responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] + + # Filter log events that match the required `property_id` + for response in responses: + for event in response["events"]: + if (ev := json.loads(event["message"])).get('detail', {}).get('property_id', '') == property_id: + yield ev + + +def clean_ddb(table_name, property_id): + ddb.delete_item(TableName=table_name, Key={ 'property_id': { 'S': property_id } }) diff --git a/unicorn_contracts/tests/integration/cwlogs.py b/unicorn_contracts/tests/integration/cwlogs.py new file mode 100644 index 0000000..870410d --- /dev/null +++ b/unicorn_contracts/tests/integration/cwlogs.py @@ -0,0 +1,78 @@ +import json +from datetime import datetime, timedelta +from time import sleep, time + +import boto3 +from pathlib import Path + +from yaml import load +# From: https://pyyaml.org/wiki/PyYAMLDocumentation +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + +DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' +STACK_OUTPUTS = dict() + +cfn = boto3.client("cloudformation") + + +def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: + with open(samconfig, 'r') as f: + conf = load(f, Loader=Loader) + stack_name = conf['default']['global']['parameters']['stack_name'] + + return stack_name + + +def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: + """ + Get the value of an output + """ + + if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): + try: + response = cfn.describe_stacks(StackName=stack_name) + except Exception as e: + raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e + + outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} + STACK_OUTPUTS[stack_name] = outputs + + try: + return outputs[output_name] + except KeyError as e: + raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e + + + +################################################################################ + +cwl = boto3.client('logs') + +def get_cw_logs_value(eb_log_group, property_id) -> str: + startTime = int((datetime.today() - timedelta(hours=5)).timestamp()) + endTime = int(datetime.now().timestamp()) + + query = f'fields @message | filter detail.property_id = "{property_id}" | limit 1' + # query = 'fields @message | filter detail.property_id = "usa/anytown/main-street/2156" | limit 1' + print(query) + res = cwl.start_query(logGroupName=eb_log_group, startTime=startTime, endTime=endTime, + queryString=query) + + timeout = time() + 60*5 # 5 minutes from now + response = None + while response is None or response['status'] == 'Running': + if time() > timeout: + raise Exception('Timed out while waiting for CW Logs results') + sleep(1) + response = cwl.get_query_results(queryId=res['queryId']) + + # log_entry = [x for x in filter(lambda r: r['field'] == '@message', response['results'][0])][0] + return response['results'] + + +if __name__ == '__main__': + result = get_cw_logs_value('uni-prop-local-contract-UnicornContractsCatchAllLogGroup-Cp7ilmQQw4o7', 'usa/anytown/main-street/2156') + print(json.loads(result[0][0]['value'])) \ No newline at end of file diff --git a/unicorn_contracts/tests/integration/test_create_contract_apigw.py b/unicorn_contracts/tests/integration/test_create_contract_apigw.py index 85fbd18..eaedaf9 100644 --- a/unicorn_contracts/tests/integration/test_create_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_create_contract_apigw.py @@ -1,44 +1,60 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import List + +from time import sleep +from random import randint import requests from unittest import TestCase -from . import get_stack_output +from . import get_stack_output, get_cw_logs_values, clean_ddb +from . import get_event_payload, override_payload_number class TestCreateContract(TestCase): api_endpoint: str + eb_log_group: str + contracts_table: str + properties: List[str] def setUp(self) -> None: self.api_endpoint = get_stack_output('ApiUrl') + self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") + self.contracts_table = get_stack_output('ContractsTableName') + self.properties = list() + + def tearDown(self) -> None: + for i in self.properties: + clean_ddb(self.contracts_table, i) - def create_contract_invalid_payload_1(self): + def test_create_contract_invalid_payload_1(self): """ Call the API Gateway endpoint and check the response """ - payload = { - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" - } - response = requests.post(f'{self.api_endpoint}contracts', json = payload) + payload = get_event_payload('create_contract_invalid_payload_1') + response = requests.post(f'{self.api_endpoint}contract', json = payload) self.assertEqual(response.status_code, 400) + self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) + def test_create_contract_valid_payload_1(self): - payload = { - "address": { - "country": "USA", - "city": "Anytown", - "street": "Main Street", - "number": 111 - }, - "seller_name": "John Smith", - "property_id": "usa/anytown/main-street/111" - } - - response = requests.put(f'{self.api_endpoint}contract', json=payload) + prop_number = randint(1, 9999) + payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) + + # Call API to create new Contract + response = requests.post(f'{self.api_endpoint}contract', json=payload) + self.properties.append(payload['property_id']) + self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) + + sleep(5) + try: + eb_event = next(get_cw_logs_values(self.eb_log_group, payload['property_id'])) + except Exception: + raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') + + self.assertEqual(eb_event['detail']['contract_status'], "DRAFT") diff --git a/unicorn_contracts/tests/integration/test_update_contract_apigw.py b/unicorn_contracts/tests/integration/test_update_contract_apigw.py index a466bb3..f73d495 100644 --- a/unicorn_contracts/tests/integration/test_update_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_update_contract_apigw.py @@ -1,42 +1,68 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import List + +from time import sleep +from random import randint import requests from unittest import TestCase -from . import get_stack_output +from . import get_stack_output, get_cw_logs_values, clean_ddb +from . import get_event_payload, override_payload_number class TestUpdateContract(TestCase): api_endpoint: str + eb_log_group: str + contracts_table: str + properties: List[str] def setUp(self) -> None: self.api_endpoint = get_stack_output('ApiUrl') + self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") + self.contracts_table = get_stack_output('ContractsTableName') + self.properties = list() + + def tearDown(self) -> None: + for i in self.properties: + clean_ddb(self.contracts_table, i) # NOTE: This test is not working as it supposed to. # Need a way for OpenApi Spec to validate extra keys on payload - def test_update_existing_contract_invalid_payload_1(self): - payload = { - "property_id": "usa/anytown/main-street/123", - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" - } + # def test_update_existing_contract_invalid_payload_1(self): + # payload = get_event_payload('update_existing_contract_invalid_payload_1') + + # response = requests.put(f'{self.api_endpoint}contract', json=payload) + # self.assertEqual(response.status_code, 400) - response = requests.put(f'{self.api_endpoint}contract', json=payload) - self.assertEqual(response.status_code, 200) - self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) def test_update_existing_contract_valid_payload(self): - payload = payload = { - "property_id": "usa/anytown/main-street/123" - } + prop_number = randint(1, 9999) + payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) - response = requests.put(f'{self.api_endpoint}contract', json=payload) + # Call API to create new Contract + response = requests.post(f'{self.api_endpoint}contract', json=payload) + self.properties.append(payload['property_id']) + + # Call API to update contract + response = requests.put(f'{self.api_endpoint}contract', json={'property_id': payload['property_id']}) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) + sleep(10) + try: + events_contract_statuses = [e['detail']['contract_status'] + for e in get_cw_logs_values(self.eb_log_group, payload['property_id'])] + events_contract_statuses.sort() + except Exception: + raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') + + # self.assertTrue("APPROVED" in events_contract_statuses) + self.assertListEqual(events_contract_statuses, ['APPROVED', 'DRAFT']) + + def test_update_missing_contract_invalid_payload_1(self): payload = { "add": "St.1 , Building 10", @@ -48,6 +74,7 @@ def test_update_missing_contract_invalid_payload_1(self): self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) + def test_update_missing_contract_valid_payload(self): payload = { "property_id": "usa/some_other_town/street/878828" From e96e9c9724092dc8652bfd839e430cd0f411ad11 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 31 Aug 2023 14:37:56 +1000 Subject: [PATCH 11/87] chore: add missing licensing header --- unicorn_contracts/tests/integration/__init__.py | 2 ++ unicorn_contracts/tests/integration/cwlogs.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/unicorn_contracts/tests/integration/__init__.py b/unicorn_contracts/tests/integration/__init__.py index 006dbae..a76257e 100644 --- a/unicorn_contracts/tests/integration/__init__.py +++ b/unicorn_contracts/tests/integration/__init__.py @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 from typing import Iterator import json diff --git a/unicorn_contracts/tests/integration/cwlogs.py b/unicorn_contracts/tests/integration/cwlogs.py index 870410d..d93d2ff 100644 --- a/unicorn_contracts/tests/integration/cwlogs.py +++ b/unicorn_contracts/tests/integration/cwlogs.py @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 import json from datetime import datetime, timedelta from time import sleep, time From 843e573b838b89fcdd8e071b765a67a86adae16e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 1 Sep 2023 14:24:40 +1000 Subject: [PATCH 12/87] chore: typos and file styling --- .../src/contracts_service/contract_event_handler.py | 2 +- unicorn_contracts/tests/unit/conftest.py | 3 +-- unicorn_contracts/tests/unit/event_generator.py | 2 ++ unicorn_contracts/tests/unit/test_contract_event_handler.py | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 31bf3fb..b8c8e07 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -35,7 +35,7 @@ @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore -@logger.inject_lambda_context() +@logger.inject_lambda_context @tracer.capture_method @event_source(data_class=SQSEvent) def lambda_handler(event: SQSEvent, context: LambdaContext): diff --git a/unicorn_contracts/tests/unit/conftest.py b/unicorn_contracts/tests/unit/conftest.py index 7e098d1..e1fa076 100644 --- a/unicorn_contracts/tests/unit/conftest.py +++ b/unicorn_contracts/tests/unit/conftest.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os import boto3 @@ -61,4 +60,4 @@ def lambda_context(): context._log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) # context._client_context=None - return context \ No newline at end of file + return context diff --git a/unicorn_contracts/tests/unit/event_generator.py b/unicorn_contracts/tests/unit/event_generator.py index 5601a21..334380c 100644 --- a/unicorn_contracts/tests/unit/event_generator.py +++ b/unicorn_contracts/tests/unit/event_generator.py @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 from typing import Any, List import json diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py index 3deab43..3673dcc 100644 --- a/unicorn_contracts/tests/unit/test_contract_event_handler.py +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os from importlib import reload From 5f696953922c74abe69746e5fb22da6c9a43ae3e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 1 Sep 2023 14:38:51 +1000 Subject: [PATCH 13/87] fix: remote unused test file, fixed naming convention of subscriber EB Policy --- unicorn_contracts/subscriber-policies.yaml | 2 +- unicorn_contracts/tests/integration/cwlogs.py | 80 ------------------- 2 files changed, 1 insertion(+), 81 deletions(-) delete mode 100644 unicorn_contracts/tests/integration/cwlogs.py diff --git a/unicorn_contracts/subscriber-policies.yaml b/unicorn_contracts/subscriber-policies.yaml index ea3daf6..da13faa 100644 --- a/unicorn_contracts/subscriber-policies.yaml +++ b/unicorn_contracts/subscriber-policies.yaml @@ -20,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBus}}" - StatementId: !Sub CreateRulePolicy-${Stage} + StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" Statement: Effect: Allow Principal: diff --git a/unicorn_contracts/tests/integration/cwlogs.py b/unicorn_contracts/tests/integration/cwlogs.py deleted file mode 100644 index d93d2ff..0000000 --- a/unicorn_contracts/tests/integration/cwlogs.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -import json -from datetime import datetime, timedelta -from time import sleep, time - -import boto3 -from pathlib import Path - -from yaml import load -# From: https://pyyaml.org/wiki/PyYAMLDocumentation -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader - -DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' -STACK_OUTPUTS = dict() - -cfn = boto3.client("cloudformation") - - -def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: - with open(samconfig, 'r') as f: - conf = load(f, Loader=Loader) - stack_name = conf['default']['global']['parameters']['stack_name'] - - return stack_name - - -def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: - """ - Get the value of an output - """ - - if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): - try: - response = cfn.describe_stacks(StackName=stack_name) - except Exception as e: - raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e - - outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} - STACK_OUTPUTS[stack_name] = outputs - - try: - return outputs[output_name] - except KeyError as e: - raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e - - - -################################################################################ - -cwl = boto3.client('logs') - -def get_cw_logs_value(eb_log_group, property_id) -> str: - startTime = int((datetime.today() - timedelta(hours=5)).timestamp()) - endTime = int(datetime.now().timestamp()) - - query = f'fields @message | filter detail.property_id = "{property_id}" | limit 1' - # query = 'fields @message | filter detail.property_id = "usa/anytown/main-street/2156" | limit 1' - print(query) - res = cwl.start_query(logGroupName=eb_log_group, startTime=startTime, endTime=endTime, - queryString=query) - - timeout = time() + 60*5 # 5 minutes from now - response = None - while response is None or response['status'] == 'Running': - if time() > timeout: - raise Exception('Timed out while waiting for CW Logs results') - sleep(1) - response = cwl.get_query_results(queryId=res['queryId']) - - # log_entry = [x for x in filter(lambda r: r['field'] == '@message', response['results'][0])][0] - return response['results'] - - -if __name__ == '__main__': - result = get_cw_logs_value('uni-prop-local-contract-UnicornContractsCatchAllLogGroup-Cp7ilmQQw4o7', 'usa/anytown/main-street/2156') - print(json.loads(result[0][0]['value'])) \ No newline at end of file From 1c49f9ade21f97ed9a5f56d4f71fa03e84013165 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:06:03 +1000 Subject: [PATCH 14/87] chore: remove unused coverage file --- unicorn_web/.coveragerc | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 unicorn_web/.coveragerc diff --git a/unicorn_web/.coveragerc b/unicorn_web/.coveragerc deleted file mode 100644 index 3dbfbb4..0000000 --- a/unicorn_web/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -omit = tests/* \ No newline at end of file From ae8f05c26e1374cbf9a28dcccd9cc6bc2f8a1fa5 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:22:11 +1000 Subject: [PATCH 15/87] feat: Updated unicorn_web to v2 architecture, implemented integration and Curl tests --- unicorn_web/Makefile | 47 +++- unicorn_web/api.yaml | 263 ++++++++++-------- unicorn_web/data/load_data.sh | 2 +- unicorn_web/poetry.lock | 48 ++-- unicorn_web/pyproject.toml | 1 + unicorn_web/samconfig.yaml | 50 +++- .../publication_approved_event_handler.py | 1 - .../request_approval_function.py | 214 ++++++-------- .../property_search_function.py | 9 +- unicorn_web/subscriber-policies.yaml | 46 +++ unicorn_web/subscriptions.yaml | 52 ++-- unicorn_web/template.yaml | 70 +---- unicorn_web/tests/integration/__init__.py | 79 ++++++ .../create_contract_invalid_payload_1.json | 5 + .../create_contract_valid_payload_1.json | 10 + ...e_existing_contract_invalid_payload_1.json | 6 + ...ate_existing_contract_valid_payload_1.json | 3 + ...te_missing_contract_invalid_payload_1.json | 5 + ...date_missing_contract_valid_payload_1.json | 3 + .../test_request_approval_apigw.py | 60 ++++ .../tests/integration/test_search_apigw.py | 60 ++++ .../transformations/ddb_contract.jq | 14 + unicorn_web/tests/unit/conftest.py | 39 ++- unicorn_web/tests/unit/event_generator.py | 177 ++++++++++++ .../unit/events/request_approval_event.json | 80 +----- unicorn_web/tests/unit/helper.py | 49 +++- unicorn_web/tests/unit/lambda_context.py | 18 -- ...test_publication_approved_event_handler.py | 46 +-- .../unit/test_request_approval_function.py | 163 ++++++----- .../tests/unit/test_search_function.py | 192 ++++++------- 30 files changed, 1124 insertions(+), 688 deletions(-) create mode 100644 unicorn_web/subscriber-policies.yaml create mode 100644 unicorn_web/tests/integration/__init__.py create mode 100644 unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json create mode 100644 unicorn_web/tests/integration/events/create_contract_valid_payload_1.json create mode 100644 unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json create mode 100644 unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json create mode 100644 unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json create mode 100644 unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json create mode 100644 unicorn_web/tests/integration/test_request_approval_apigw.py create mode 100644 unicorn_web/tests/integration/test_search_apigw.py create mode 100644 unicorn_web/tests/integration/transformations/ddb_contract.jq create mode 100644 unicorn_web/tests/unit/event_generator.py delete mode 100644 unicorn_web/tests/unit/lambda_context.py diff --git a/unicorn_web/Makefile b/unicorn_web/Makefile index d6c79ae..7a9ecb5 100644 --- a/unicorn_web/Makefile +++ b/unicorn_web/Makefile @@ -1,20 +1,50 @@ +#### Global Variables +stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.yaml) + + +#### Test Variables +apiUrl = $(call cf_output,$(stackName),ApiUrl) + + +#### Build/Deploy Tasks deps: poetry install build: + swagger-cli validate api.yaml + sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export -f requirements.txt --without-hashes --output src/approvals_service/requirements.txt poetry export -f requirements.txt --without-hashes --output src/search_service/requirements.txt sam build -c $(DOCKER_OPTS) deploy: build - sam deploy --no-confirm-changeset + sam deploy -test: unit-test + +#### Tests +test: unit-test integration-test unit-test: deps poetry run pytest tests/unit/ +integration-test: deps + poetry run pytest tests/integration/ + +curl-test: + $(call mcurl,GET,search/usa/anytown) + $(call mcurl,GET,search/usa/anytown/main-street) + $(call mcurl,GET,properties/usa/anytown/main-street/111) + @echo "[DONE]" + + +#### Utilities +sync: + sam sync --stack-name $(stackName) --watch + +logs: + sam logs --stack-name $(stackName) -t + clean: find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true @@ -28,3 +58,16 @@ ci_init: poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt --with dev poetry run pip install -r src/requirements.txt poetry install -n + + +#### Helper Functions +define mcurl + curl -s -X $(1) -H "Content-type: application/json" $(apiUrl)$(2) | jq +endef + +define cf_output + $(shell aws cloudformation describe-stacks \ + --output text \ + --stack-name $(1) \ + --query 'Stacks[0].Outputs[?OutputKey==`$(2)`].OutputValue') +endef diff --git a/unicorn_web/api.yaml b/unicorn_web/api.yaml index ae530c3..5203ba6 100644 --- a/unicorn_web/api.yaml +++ b/unicorn_web/api.yaml @@ -14,16 +14,12 @@ paths: required: true responses: "200": - description: "200 response" - content: - application/json: - schema: - $ref: "#/components/schemas/Empty" + $ref: '#/components/responses/Empty' x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] - httpMethod: "POST" + httpMethod: POST uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornWebIngestQueue.QueueName}" responses: @@ -35,149 +31,107 @@ paths: integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" requestTemplates: application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" - passthroughBehavior: "never" - type: "aws" - options: - responses: - "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" - content: - application/json: - schema: - $ref: "#/components/schemas/Empty" - x-amazon-apigateway-integration: - responses: - default: - statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" - method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Origin: "'*'" - requestTemplates: - application/json: '{"statusCode": 200}' - passthroughBehavior: "when_no_match" - type: "mock" + passthroughBehavior: never + type: aws /search/{country}/{city}: get: parameters: - - name: "country" - in: "path" + - name: country + in: path required: true schema: - type: "string" - - name: "city" - in: "path" + type: string + - name: city + in: path required: true schema: - type: "string" + type: string + responses: + "200": + $ref: '#/components/responses/ListPropertiesResponseBody' x-amazon-apigateway-integration: credentials: Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] - httpMethod: "POST" + httpMethod: POST uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SearchFunction.Arn}/invocations" responses: default: statusCode: "200" - passthroughBehavior: "when_no_match" - contentHandling: "CONVERT_TO_TEXT" - type: "aws_proxy" - options: + passthroughBehavior: when_no_match + contentHandling: CONVERT_TO_TEXT + type: aws_proxy + /search/{country}/{city}/{street}: + get: + parameters: + - name: country + in: path + required: true + schema: + type: string + - name: city + in: path + required: true + schema: + type: string + - name: street + in: path + required: true + schema: + type: string responses: "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" - content: - application/json: - schema: - $ref: "#/components/schemas/Empty" + $ref: '#/components/responses/ListPropertiesResponseBody' x-amazon-apigateway-integration: + credentials: + Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] + httpMethod: POST + uri: + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SearchFunction.Arn}/invocations" responses: default: statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" - method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Origin: "'*'" - requestTemplates: - application/json: '{"statusCode": 200}' - passthroughBehavior: "when_no_match" - type: "mock" - /search/{country}/{city}/{street}: + passthroughBehavior: when_no_match + contentHandling: CONVERT_TO_TEXT + type: aws_proxy + /properties/{country}/{city}/{street}/{number}: get: parameters: - - name: "street" - in: "path" + - name: country + in: path + required: true + schema: + type: string + - name: city + in: path required: true schema: - type: "string" - - name: "country" - in: "path" + type: string + - name: street + in: path required: true schema: - type: "string" - - name: "city" - in: "path" + type: string + - name: number + in: path required: true schema: - type: "string" + type: string + responses: + "200": + $ref: '#/components/responses/PropertyDetailsResponseBody' x-amazon-apigateway-integration: credentials: Fn::GetAtt: [UnicornWebApiIntegrationRole, Arn] - httpMethod: "POST" + httpMethod: POST uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SearchFunction.Arn}/invocations" - passthroughBehavior: "when_no_match" - type: "aws_proxy" - options: - responses: - "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - schema: - type: "string" - Access-Control-Allow-Methods: - schema: - type: "string" - Access-Control-Allow-Headers: - schema: - type: "string" - content: - application/json: - schema: - $ref: "#/components/schemas/Empty" - x-amazon-apigateway-integration: responses: default: statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" - method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Origin: "'*'" - requestTemplates: - application/json: '{"statusCode": 200}' - passthroughBehavior: "when_no_match" - type: "mock" + passthroughBehavior: when_no_match + contentHandling: CONVERT_TO_TEXT + type: aws_proxy components: schemas: PublicationEvaluationRequestModel: @@ -186,14 +140,91 @@ components: type: "object" properties: property_id: - type: "string" + type: string PublicationEvaluationResponseModel: required: - "result" type: "object" properties: result: - type: "string" + type: string + PropertyAddress: + type: object + required: + - country + - city + - street + - number + properties: + country: + type: string + city: + type: string + street: + type: string + number: + type: string + PropertyDetails: + type: object + required: + - description + - images + - status + properties: + description: + type: string + images: + type: array + items: + type: string + PropertyOffer: + type: object + required: + - currency + - listprice + - contract + - status + properties: + contract: + type: string + listprice: + type: string + currency: + type: string + Property: + allOf: + - $ref: "#/components/schemas/PropertyAddress" + - $ref: "#/components/schemas/PropertyDetails" + - $ref: "#/components/schemas/PropertyOffer" + - type: object + properties: + status: + type: string + responses: + ListPropertiesResponseBody: + description: 'OK' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + allOf: + - $ref: "#/components/schemas/PropertyAddress" + - $ref: "#/components/schemas/PropertyOffer" + PropertyDetailsResponseBody: + description: 'OK' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Property' Empty: - title: "Empty Schema" - type: "object" \ No newline at end of file + description: 'OK' + content: + application/json: + schema: + title: "Empty Schema" + type: "object" diff --git a/unicorn_web/data/load_data.sh b/unicorn_web/data/load_data.sh index 07c22d5..6f181e1 100755 --- a/unicorn_web/data/load_data.sh +++ b/unicorn_web/data/load_data.sh @@ -3,7 +3,7 @@ ROOT_DIR="$(cd -- "$(dirname "$0")/../" >/dev/null 2>&1 ; pwd -P )" STACK_NAME="$(yq -oy '.default.global.parameters.stack_name' $ROOT_DIR/samconfig.yaml)" -JSON_FILE="$(cd -- "$ROOT_DIR/data/property_data.json" +JSON_FILE="$ROOT_DIR/data/property_data.json" echo "JSON_FILE: '${JSON_FILE}'" DDB_TBL_NAME="$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query 'Stacks[0].Outputs[?OutputKey==`WebTableName`].OutputValue' --output text)" diff --git a/unicorn_web/poetry.lock b/unicorn_web/poetry.lock index cb113f4..8ac5391 100644 --- a/unicorn_web/poetry.lock +++ b/unicorn_web/poetry.lock @@ -1,14 +1,25 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "arnparse" +version = "0.0.2" +description = "Parse ARNs using Python" +optional = false +python-versions = "*" +files = [ + {file = "arnparse-0.0.2-py2.py3-none-any.whl", hash = "sha256:b0906734e4b8f19e39b1e32944c6cd6274b6da90c066a83882ac7a11d27553e0"}, + {file = "arnparse-0.0.2.tar.gz", hash = "sha256:cb87f17200d07121108a9085d4a09cc69a55582647776b9a917b0b1f279db8f8"}, +] + [[package]] name = "aws-lambda-powertools" -version = "2.23.0" +version = "2.23.1" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, - {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, + {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, + {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, ] [package.dependencies] @@ -18,6 +29,7 @@ typing-extensions = ">=4.6.2,<5.0.0" [package.extras] all = ["aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "pydantic (>=1.8.2,<2.0.0)"] aws-sdk = ["boto3 (>=1.20.32,<2.0.0)"] +datadog = ["datadog-lambda (>=4.77.0,<5.0.0)"] parser = ["pydantic (>=1.8.2,<2.0.0)"] tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] @@ -39,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.32" +version = "1.28.37" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.32-py3-none-any.whl", hash = "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3"}, - {file = "boto3-1.28.32.tar.gz", hash = "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28"}, + {file = "boto3-1.28.37-py3-none-any.whl", hash = "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6"}, + {file = "boto3-1.28.37.tar.gz", hash = "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2"}, ] [package.dependencies] -botocore = ">=1.31.32,<1.32.0" +botocore = ">=1.31.37,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.32" +version = "1.31.37" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.32-py3-none-any.whl", hash = "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c"}, - {file = "botocore-1.31.32.tar.gz", hash = "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664"}, + {file = "botocore-1.31.37-py3-none-any.whl", hash = "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06"}, + {file = "botocore-1.31.37.tar.gz", hash = "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de"}, ] [package.dependencies] @@ -507,13 +519,13 @@ files = [ [[package]] name = "moto" -version = "4.1.15" +version = "4.2.0" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.1.15-py2.py3-none-any.whl", hash = "sha256:3fbcf91090692c30117d275fb34b48a075a6f65d4712ba6c4d004ffab976db46"}, - {file = "moto-4.1.15.tar.gz", hash = "sha256:272236d312457b324c645741ee589924fd61a96b84680dc2e607f8663c563551"}, + {file = "moto-4.2.0-py2.py3-none-any.whl", hash = "sha256:e4835912f05627b6a53b938562b717122230fb038d023819133f8526f60ed0a7"}, + {file = "moto-4.2.0.tar.gz", hash = "sha256:00fbae396fc48c3596e47b4e3267c1a41ca01c968de023beb68e774c63910b58"}, ] [package.dependencies] @@ -566,13 +578,13 @@ files = [ [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -945,4 +957,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "99c790c06737375a60aad7fa149e27f638d3dbe2f14389872c1a70164237f480" +content-hash = "450945a735b7cd52c30f0204d9d6a45a8222696c8f8384334c04094d928d359c" diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index 183191b..ea93a86 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -24,6 +24,7 @@ coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" +arnparse = "^0.0.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index 928d97c..138b1d9 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -1,9 +1,10 @@ version: 0.1 + default: global: parameters: - stack_name: "uni-prop-local-web" - s3_prefix: "uni-prop-local-web" + stack_name: uni-prop-local-web + s3_prefix: uni-prop-local-web resolve_s3: true resolve_image_repositories: true build: @@ -12,32 +13,53 @@ default: parallel: true deploy: parameters: + disable_rollback: true confirm_changeset: false + fail_on_empty_changeset: false capabilities: CAPABILITY_IAM - disable_rollback: true - parameter_overrides: Stage="Local" - image_repositories: [] + parameter_overrides: + - "Stage=Local" validate: - parameters: - lint: true + parameters: + lint: true sync: parameters: watch: true local_start_api: parameters: - warm_containers: "EAGER" + warm_containers: EAGER local_start_lambda: parameters: - warm_containers: "EAGER" + warm_containers: EAGER + +subscribers: + global: + parameters: + stack_name: uni-prop-local-web-subscribers + s3_prefix: uni-prop-local-web-subscribers + resolve_s3: true + deploy: + parameters: + template_file: subscriber-policies.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" subscriptions: global: - parameters: + parameters: stack_name: uni-prop-local-web-subscriptions + s3_prefix: uni-prop-local-web-subscriptions + resolve_s3: true deploy: parameters: - parameter_overrides: Stage="Local" - capabilities: CAPABILITY_IAM - template_file: "subscriptions.yaml" + template_file: subscriptions.yaml + confirm_changeset: false fail_on_empty_changeset: false - on_failure: ROLLBACK \ No newline at end of file + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" diff --git a/unicorn_web/src/approvals_service/publication_approved_event_handler.py b/unicorn_web/src/approvals_service/publication_approved_event_handler.py index a441b3c..57ac7a6 100644 --- a/unicorn_web/src/approvals_service/publication_approved_event_handler.py +++ b/unicorn_web/src/approvals_service/publication_approved_event_handler.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os import boto3 diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 1c9c164..2a8a5a4 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os import re import json @@ -8,26 +7,25 @@ import boto3 from botocore.exceptions import ClientError -# import aws_lambda_powertools.event_handler.exceptions -from aws_lambda_powertools.logging import Logger, correlation_paths +from aws_lambda_powertools.logging import Logger from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.metrics import Metrics, MetricUnit -from aws_lambda_powertools.event_handler import content_types -from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, Response -from aws_lambda_powertools.event_handler.exceptions import NotFoundError, InternalServerError, BadRequestError +from aws_lambda_powertools.utilities.data_classes import event_source, SQSEvent +from aws_lambda_powertools.utilities.typing import LambdaContext # Initialise Environment variables if (SERVICE_NAMESPACE := os.environ.get('SERVICE_NAMESPACE')) is None: - raise InternalServerError('SERVICE_NAMESPACE environment variable is undefined') + raise EnvironmentError('SERVICE_NAMESPACE environment variable is undefined') if (DYNAMODB_TABLE := os.environ.get('DYNAMODB_TABLE')) is None: - raise InternalServerError('DYNAMODB_TABLE environment variable is undefined') + raise EnvironmentError('DYNAMODB_TABLE environment variable is undefined') if (EVENT_BUS := os.environ.get('EVENT_BUS')) is None: - raise InternalServerError('EVENT_BUS environment variable is undefined') + raise EnvironmentError('EVENT_BUS environment variable is undefined') EXPRESSION = r"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+" TARGET_STATE = 'PENDING' + # Initialise PowerTools logger: Logger = Logger() tracer: Tracer = Tracer() @@ -38,58 +36,86 @@ dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore -app = ApiGatewayResolver() - -@app.post('/request_approval') @tracer.capture_method -def request_approval(): - """Emits event that user requested a property approval - - Returns - ------- - Confirmation that the event was emitted successfully - """ - logger.info('Call to request_approval') - +def send_eb_event(detail_type, resources, detail): try: - raw_data = app.current_event.json_body - except json.JSONDecodeError as e: - error_msg = f"Unable to parse event input as JSON: {e}" + entry = {'EventBusName': EVENT_BUS, + 'Source': SERVICE_NAMESPACE, + 'DetailType': detail_type, + 'Resources': resources, + 'Detail': json.dumps(detail)} + + response = event_bridge.put_events(Entries=[entry]) + except ClientError as e: + error_msg = f"Unable to send event to Event Bus: {e}" logger.error(error_msg) - raise BadRequestError(error_msg) + raise Exception(error_msg) - property_id = raw_data['property_id'] + failed_count = response['FailedEntryCount'] - if not re.fullmatch(EXPRESSION, property_id): - error_msg = f"Input invalid; must conform to regular expression: {EXPRESSION}" + if failed_count > 0: + error_msg = f"Error sending requests to Event Bus; {failed_count} message(s) failed" logger.error(error_msg) - raise BadRequestError(error_msg) + raise Exception(error_msg) - country, city, street, number = property_id.split('/') + entry_count = len(response['Entries']) + logger.info(f"Sent event to EventBridge; {failed_count} records failed; {entry_count} entries received") + return response - pk_details = f"{country}#{city}".replace(' ', '-').lower() - pk = f"PROPERTY#{pk_details}" - sk = f"{street}#{str(number)}".replace(' ', '-').lower() +@tracer.capture_method +def get_property_from_ddb(pk: str, sk: str) -> dict: response = table.get_item( - Key={ - 'PK': pk, - 'SK': sk - }, - AttributesToGet=['currency', 'status', 'listprice', 'contract', 'country', 'city', 'number', 'images', + Key={ 'PK': pk, 'SK': sk }, + AttributesToGet=['currency', 'status', 'listprice', 'contract', + 'country', 'city', 'number', 'images', 'description', 'street'] ) if 'Item' not in response: logger.info(f"No item found in table {DYNAMODB_TABLE} with PK {pk} and SK {sk}") - raise NotFoundError(f"No property found in database with the requested property id") + return dict() - item = response['Item'] + return response['Item'] - status = item.pop('status') - if status in [ 'APPROVED', 'DECLINED', 'PENDING' ]: - return {'result': f"Property is already {status}; no action taken" } +@tracer.capture_method +def update_property_status(pk: str, sk: str, state: str) -> bool: + logger.info(f"Updating status of property {pk},{sk} in DynamoDB to {state}") + response = table.update_item( + Key={ 'PK': pk, 'SK': sk }, + AttributeUpdates={ + 'status': { + 'Value': state, + 'Action': 'PUT', + } + }, + ) + return response['ResponseMetadata']['HTTPStatusCode'] == 22 + + +@tracer.capture_method +def request_approval(raw_data: dict): + property_id = raw_data['property_id'] + + # Validate Property ID + if not re.fullmatch(EXPRESSION, property_id): + error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" + logger.error(error_msg) + return + + country, city, street, number = property_id.split('/') + + # Construct DDB PK & SK keys for this property + pk_details = f"{country}#{city}".replace(' ', '-').lower() + pk = f"PROPERTY#{pk_details}" + sk = f"{street}#{str(number)}".replace(' ', '-').lower() + + item = get_property_from_ddb(pk=pk, sk=sk) + + if (status := item.pop('status')) in [ 'APPROVED', 'DECLINED', 'PENDING' ]: + logger.info(f"Property '{property_id}' is already {status}; no action taken") + return item['property_id'] = property_id item['address'] = { @@ -101,93 +127,23 @@ def request_approval(): item['status'] = TARGET_STATE item['listprice'] = int(item['listprice']) - try: - event_bridge_response = event_bridge.put_events( - Entries=[ - { - 'Source': SERVICE_NAMESPACE, - 'DetailType': 'PublicationApprovalRequested', - 'Resources': [property_id], - 'Detail': json.dumps(item), - 'EventBusName': EVENT_BUS, - }, - ] - ) - except ClientError as e: - error_msg = f"Unable to send event to Event Bus: {e}" - logger.error(error_msg) - raise InternalServerError(error_msg) - - failed_count = event_bridge_response['FailedEntryCount'] - - if failed_count > 0: - error_msg = f"Error sending requests to Event Bus; {failed_count} message(s) failed" - logger.error(error_msg) - raise InternalServerError(error_msg) - - entry_count = len(event_bridge_response['Entries']) - logger.info(f"Sent event to EventBridge; {failed_count} records failed; {entry_count} entries received") + send_eb_event(detail_type='PublicationApprovalRequested', + resources=[property_id], detail=item) metrics.add_metric(name='ApprovalsRequested', unit=MetricUnit.Count, value=1) - logger.info(f"Storing new property in DynamoDB with PK {pk} and SK {sk}") - dynamodb_response = table.update_item( - Key={ - 'PK': pk, - 'SK': sk, - }, - AttributeUpdates={ - 'status': { - 'Value': TARGET_STATE, - 'Action': 'PUT', - } - }, - ) - http_status_code = dynamodb_response['ResponseMetadata']['HTTPStatusCode'] - logger.info(f"Stored item in DynamoDB; responded with status code {http_status_code}") - - return {'result': 'Approval Requested'} - - -@app.exception_handler(ClientError) -def handle_service_error(ex: ClientError): - """Handles any error coming from a remote service request made through Boto3 (ClientError) - - Parameters - ---------- - ex : Boto3 error occuring during an AWS API call anywhere in this Lambda function - - Returns - ------- - Specific HTTP error code to be returned to the client as well as a friendly error message - """ - error_code = ex.response['Error']['Code'] - http_status_code = ex.response['ResponseMetadata']['HTTPStatusCode'] - error_message = ex.response['Error']['Message'] - logger.exception(f"EXCEPTION {error_code} ({http_status_code}): {error_message}") - return Response( - status_code=http_status_code, - content_type=content_types.TEXT_PLAIN, - body=error_code - ) + update_property_status(pk=pk, sk=sk, state=TARGET_STATE) -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) # type: ignore -@tracer.capture_lambda_handler # type: ignore -@metrics.log_metrics -def lambda_handler(event, context): - """Main entry point for PropertyWeb lambda function - - Parameters - ---------- - event : API Gateway Lambda Proxy Request - The event passed to the function. - context : AWS Lambda Context - The context for the Lambda function. - - Returns - ------- - API Gateway Lambda Proxy Response - HTTP response object with Contract and Property ID - """ - logger.info(event) - return app.resolve(event, context) +@metrics.log_metrics(capture_cold_start_metric=True) # type: ignore +@logger.inject_lambda_context +@tracer.capture_method +@event_source(data_class=SQSEvent) +def lambda_handler(event: SQSEvent, context: LambdaContext): + # Multiple records can be delivered in a single event + for record in event.records: + http_method = record.message_attributes.get('HttpMethod', {}).get('stringValue') + + if http_method == 'POST': + request_approval(record.json_body) + else: + raise Exception(f'Unable to handle HttpMethod {http_method}') \ No newline at end of file diff --git a/unicorn_web/src/search_service/property_search_function.py b/unicorn_web/src/search_service/property_search_function.py index 9acda1e..d29b83e 100644 --- a/unicorn_web/src/search_service/property_search_function.py +++ b/unicorn_web/src/search_service/property_search_function.py @@ -1,11 +1,11 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os import boto3 from boto3.dynamodb.conditions import Key, Attr from botocore.exceptions import ClientError + from aws_lambda_powertools.logging import Logger, correlation_paths from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.metrics import Metrics @@ -26,7 +26,6 @@ metrics: Metrics = Metrics() # Initialise boto3 clients -event_bridge = boto3.client('events') dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore @@ -121,12 +120,12 @@ def property_details(country, city, street, number): } ) if 'Item' not in response: - logger.exception(f"No property found at this address") + logger.exception(f"No property found at address {(country, city, street, number)}") raise NotFoundError item = response['Item'] status = item['status'] if status != 'APPROVED': - status_message = f"Property is not approved; current status: {status}" + status_message = f"Property is not approved; current status: {status}" logger.exception(status_message) raise NotFoundError(status_message) item.pop("PK") @@ -175,5 +174,5 @@ def lambda_handler(event, context): API Gateway Lambda Proxy Response HTTP response object with Contract and Property ID """ - logger.info(event) + # logger.info(event) return app.resolve(event, context) diff --git a/unicorn_web/subscriber-policies.yaml b/unicorn_web/subscriber-policies.yaml new file mode 100644 index 0000000..a60a361 --- /dev/null +++ b/unicorn_web/subscriber-policies.yaml @@ -0,0 +1,46 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by the Contracts Service. + +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +Resources: + # Update this policy as you get new subscribers by adding their namespace to events:source + CrossServiceCreateRulePolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBus}}" + StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" + Statement: + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - events:PutRule + - events:DeleteRule + - events:DescribeRule + - events:DisableRule + - events:EnableRule + - events:PutTargets + - events:RemoveTargets + Resource: + - !Sub + - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* + - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBus}}" + Condition: + StringEqualsIfExists: + "events:creatorAccount": "${aws:PrincipalAccount}" + StringEquals: + "events:source": + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + "Null": + "events:source": "false" diff --git a/unicorn_web/subscriptions.yaml b/unicorn_web/subscriptions.yaml index bd1963b..987edb7 100644 --- a/unicorn_web/subscriptions.yaml +++ b/unicorn_web/subscriptions.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -AWSTemplateFormatVersion: "2010-09-09" -Description: Defines EventBus subscriptions for UnicornWeb service +AWSTemplateFormatVersion: '2010-09-09' +Description: Defines the subscriptions for Parameters: Stage: @@ -13,15 +13,15 @@ Parameters: - Prod Resources: - PublicationApprovedSubscriptionRule: + PublicationEvaluationCompletedSubscriptionRule: Type: AWS::Events::Rule Properties: - Name: web.publicationapproved-properties.pubevalcompleted - Description: Publication evaluation completed subscription + Name: web.pubapprovalwf-properties.pubapprovalrequested + Description: Constract Status Changed subscription EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespaceParam}}" + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" detail-type: - PublicationEvaluationCompleted State: ENABLED @@ -30,10 +30,10 @@ Resources: Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" RoleArn: !GetAtt UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn - # This IAM role allows EventBridge to assume the permissions necessary to send events - # from the Unicorn Properties event bus to the Unicorn Web event bus. - # If the Unicorn Web wants receive events from other services, then add another policy - # the arn of the which includes the event bus arn where Unicorn Web is creating the + # This IAM role allows EventBridge to assume the permissions necessary to send events + # from the Unicorn Contracts event bus to the Unicorn Properties event bus. + # If the Unicorn Properties service wants receive events from other services, then add another policy + # the arn of the which includes the event bus arn where Unicorn Properties is creating the # subscription (rule) UnicornPropertiesEventBusToUnicornWebEventBusRole: Type: AWS::IAM::Role @@ -41,38 +41,18 @@ Resources: AssumeRolePolicyDocument: Statement: - Effect: Allow + Action: sts:AssumeRole Principal: - Service: - - events.amazonaws.com - Action: - - sts:AssumeRole + Service: events.amazonaws.com Policies: - PolicyName: PutEventsOnUnicornWebEventBus PolicyDocument: Statement: - Effect: Allow - Action: "events:PutEvents" + Action: events:PutEvents Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" - # SQS resource policy required to allow target on devops bus to send failed messages to target DLQ - # BlueServiceEventBusDlqPolicy: - # Type: AWS::SQS::QueuePolicy - # Properties: - # Queues: - # - !Ref BlueServiceEventBusDlqUrl - # PolicyDocument: - # Statement: - # - Action: - # - "SQS:SendMessage" - # Effect: "Allow" - # Resource: !Ref BlueServiceEventBusDlqArn - # Principal: - # Service: "events.amazonaws.com" - # Condition: - # ArnEquals: - # "aws:SourceArn": !GetAtt BlueServiceE2SubscriptionRule.Arn - Outputs: - BlueE2Subscription: - Description: Rule ARN for blue service event 2 subscription - Value: !GetAtt PublicationApprovedSubscriptionRule.Arn + PublicationEvaluationCompletedSubscription: + Description: Rule ARN for Property service event subscription + Value: !GetAtt PublicationEvaluationCompletedSubscriptionRule.Arn diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 0865f66..a2af72d 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -102,36 +102,14 @@ Resources: Value: !GetAtt UnicornWebEventBus.Arn ##### LAMBDA FUNCTIONS - SearchFunction: + SearchFunction: #API Type: AWS::Serverless::Function Properties: - CodeUri: src/ - Handler: search_service.property_search_function.lambda_handler + CodeUri: src/search_service/ + Handler: property_search_function.lambda_handler Policies: - DynamoDBReadPolicy: TableName: !Ref WebTable - Events: - ListPropertiesByCity: - Type: Api - Properties: - Path: /search/{country}/{city} - Method: get - RestApiId: - Ref: UnicornWebApi - ListPropertiesByStreet: - Type: Api - Properties: - Path: /search/{country}/{city}/{street} - Method: get - RestApiId: - Ref: UnicornWebApi - PropertyDetails: - Type: Api - Properties: - Path: /properties/{country}/{city}/{street}/{number} - Method: get - RestApiId: - Ref: UnicornWebApi SearchFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -144,11 +122,11 @@ Resources: - !Ref Stage - Days - RequestApprovalFunction: + RequestApprovalFunction: #SQS Type: AWS::Serverless::Function Properties: - CodeUri: src/ - Handler: approvals_service.request_approval_function.lambda_handler + CodeUri: src/approvals_service/ + Handler: request_approval_function.lambda_handler Policies: - EventBridgePutEventsPolicy: EventBusName: !Ref UnicornWebEventBus @@ -177,11 +155,11 @@ Resources: - !Ref Stage - Days - PublicationApprovedFunction: + PublicationApprovedFunction: #EB Type: AWS::Serverless::Function Properties: - CodeUri: src/ - Handler: approvals_service.publication_approved_event_handler.lambda_handler + CodeUri: src/approvals_service/ + Handler: publication_approved_event_handler.lambda_handler Policies: - DynamoDBReadPolicy: TableName: !Ref WebTable @@ -395,36 +373,6 @@ Resources: events:source: - !FindInMap [Constants, ServiceNamespace, Value] - # Event bus policy to restrict who can create rules on the event bus to subscribe to events published by the Contracts Service. - # Update this policy as you get new subscribers by adding their namespace to events:source - # PropertiesServiceRulePolicy: - # Type: "AWS::Events::EventBusPolicy" - # Properties: - # EventBusName: !Ref UnicornWebEventBus - # StatementId: !Sub CreateRulePolicy-${Stage} - # Statement: - # Effect: Allow - # Principal: - # AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" - # Action: - # - "events:PutRule" - # - "events:DeleteRule" - # - "events:DescribeRule" - # - "events:DisableRule" - # - "events:EnableRule" - # - "events:PutTargets" - # - "events:RemoveTargets" - # Resource: - # - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${UnicornWebEventBus.Name}/* - # Condition: - # StringEqualsIfExists: - # "events:creatorAccount": "${aws:PrincipalAccount}" - # StringEquals: - # "events:source": - # - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - # "Null": - # "events:source": "false" - # Catchall rule used for development purposes. UnicornWebCatchAllRule: Type: AWS::Events::Rule diff --git a/unicorn_web/tests/integration/__init__.py b/unicorn_web/tests/integration/__init__.py new file mode 100644 index 0000000..006dbae --- /dev/null +++ b/unicorn_web/tests/integration/__init__.py @@ -0,0 +1,79 @@ +from typing import Iterator + +import json +from pathlib import Path + +import boto3 +from arnparse import arnparse +from yaml import load, Loader + + +#### CONSTANTS +DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' +STACK_OUTPUTS = dict() +EVENTS_DIR = Path(__file__).parent / 'events' + + +#### AWS SDK Objects +cfn = boto3.client('cloudformation') +cwl = boto3.client('logs') +ddb = boto3.client('dynamodb') + + +def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: + with open(samconfig, 'r') as f: + conf = load(f, Loader=Loader) + stack_name = conf['default']['global']['parameters']['stack_name'] + + return stack_name + + +def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: + """ + Get the value of an output + """ + + if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): + try: + response = cfn.describe_stacks(StackName=stack_name) + except Exception as e: + raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e + + outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} + STACK_OUTPUTS[stack_name] = outputs + + try: + return outputs[output_name] + except KeyError as e: + raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e + + +def get_event_payload(file) -> dict: + return json.load(open(EVENTS_DIR / f'{file}.json', 'r')) + + +def override_payload_number(p: dict, number: int) -> dict: + p['address']['number'] = number + a = p["address"] + p['property_id'] = f'{a["country"]}/{a["city"]}/{a["street"]}/{a["number"]}'.replace(' ', '-').lower() + return p + + +def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: + group_name = arnparse(eb_log_group_arn).resource + + # Get the CW LogStream with the latest log messages + stream_response = cwl.describe_log_streams(logGroupName=group_name, orderBy='LastEventTime',descending=True,limit=3) + latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] + # Fetch log events from that stream + responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] + + # Filter log events that match the required `property_id` + for response in responses: + for event in response["events"]: + if (ev := json.loads(event["message"])).get('detail', {}).get('property_id', '') == property_id: + yield ev + + +def clean_ddb(table_name, property_id): + ddb.delete_item(TableName=table_name, Key={ 'property_id': { 'S': property_id } }) diff --git a/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json new file mode 100644 index 0000000..1f352a3 --- /dev/null +++ b/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json @@ -0,0 +1,5 @@ +{ + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json new file mode 100644 index 0000000..8a9abf2 --- /dev/null +++ b/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json @@ -0,0 +1,10 @@ +{ + "address": { + "country": "USA", + "city": "Anytown", + "street": "Main Street", + "number": 123 + }, + "seller_name": "John Smith", + "property_id": "usa/anytown/main-street/123" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json new file mode 100644 index 0000000..154d225 --- /dev/null +++ b/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json @@ -0,0 +1,6 @@ +{ + "property_id": "usa/anytown/main-street/123", + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json new file mode 100644 index 0000000..ea61b51 --- /dev/null +++ b/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json @@ -0,0 +1,3 @@ +{ + "property_id": "usa/anytown/main-street/123" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json new file mode 100644 index 0000000..1f352a3 --- /dev/null +++ b/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json @@ -0,0 +1,5 @@ +{ + "add": "St.1 , Building 10", + "sell": "John Smith", + "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json new file mode 100644 index 0000000..5793374 --- /dev/null +++ b/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json @@ -0,0 +1,3 @@ +{ + "property_id": "usa/some_other_town/street/878828" +} \ No newline at end of file diff --git a/unicorn_web/tests/integration/test_request_approval_apigw.py b/unicorn_web/tests/integration/test_request_approval_apigw.py new file mode 100644 index 0000000..eaedaf9 --- /dev/null +++ b/unicorn_web/tests/integration/test_request_approval_apigw.py @@ -0,0 +1,60 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +from typing import List + +from time import sleep +from random import randint + +import requests +from unittest import TestCase + +from . import get_stack_output, get_cw_logs_values, clean_ddb +from . import get_event_payload, override_payload_number + + +class TestCreateContract(TestCase): + api_endpoint: str + eb_log_group: str + contracts_table: str + properties: List[str] + + def setUp(self) -> None: + self.api_endpoint = get_stack_output('ApiUrl') + self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") + self.contracts_table = get_stack_output('ContractsTableName') + self.properties = list() + + def tearDown(self) -> None: + for i in self.properties: + clean_ddb(self.contracts_table, i) + + + def test_create_contract_invalid_payload_1(self): + """ + Call the API Gateway endpoint and check the response + """ + + payload = get_event_payload('create_contract_invalid_payload_1') + response = requests.post(f'{self.api_endpoint}contract', json = payload) + self.assertEqual(response.status_code, 400) + self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) + + + def test_create_contract_valid_payload_1(self): + prop_number = randint(1, 9999) + payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) + + # Call API to create new Contract + response = requests.post(f'{self.api_endpoint}contract', json=payload) + self.properties.append(payload['property_id']) + + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) + + sleep(5) + try: + eb_event = next(get_cw_logs_values(self.eb_log_group, payload['property_id'])) + except Exception: + raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') + + self.assertEqual(eb_event['detail']['contract_status'], "DRAFT") diff --git a/unicorn_web/tests/integration/test_search_apigw.py b/unicorn_web/tests/integration/test_search_apigw.py new file mode 100644 index 0000000..eaedaf9 --- /dev/null +++ b/unicorn_web/tests/integration/test_search_apigw.py @@ -0,0 +1,60 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +from typing import List + +from time import sleep +from random import randint + +import requests +from unittest import TestCase + +from . import get_stack_output, get_cw_logs_values, clean_ddb +from . import get_event_payload, override_payload_number + + +class TestCreateContract(TestCase): + api_endpoint: str + eb_log_group: str + contracts_table: str + properties: List[str] + + def setUp(self) -> None: + self.api_endpoint = get_stack_output('ApiUrl') + self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") + self.contracts_table = get_stack_output('ContractsTableName') + self.properties = list() + + def tearDown(self) -> None: + for i in self.properties: + clean_ddb(self.contracts_table, i) + + + def test_create_contract_invalid_payload_1(self): + """ + Call the API Gateway endpoint and check the response + """ + + payload = get_event_payload('create_contract_invalid_payload_1') + response = requests.post(f'{self.api_endpoint}contract', json = payload) + self.assertEqual(response.status_code, 400) + self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) + + + def test_create_contract_valid_payload_1(self): + prop_number = randint(1, 9999) + payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) + + # Call API to create new Contract + response = requests.post(f'{self.api_endpoint}contract', json=payload) + self.properties.append(payload['property_id']) + + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) + + sleep(5) + try: + eb_event = next(get_cw_logs_values(self.eb_log_group, payload['property_id'])) + except Exception: + raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') + + self.assertEqual(eb_event['detail']['contract_status'], "DRAFT") diff --git a/unicorn_web/tests/integration/transformations/ddb_contract.jq b/unicorn_web/tests/integration/transformations/ddb_contract.jq new file mode 100644 index 0000000..54e3bf8 --- /dev/null +++ b/unicorn_web/tests/integration/transformations/ddb_contract.jq @@ -0,0 +1,14 @@ +.Item | { + property_id: .property_id.S, + contract_id: .contract_id.S, + seller_name: .seller_name.S, + address: { + country: .address.M.country.S, + number: .address.M.number.N, + city: .address.M.city.S, + street: .address.M.street.S, + }, + contract_status: .contract_status.S, + contract_created: .contract_created.S, + contract_last_modified_on: .contract_last_modified_on.S +} | del(..|nulls) diff --git a/unicorn_web/tests/unit/conftest.py b/unicorn_web/tests/unit/conftest.py index 9d47f1c..e1fa076 100644 --- a/unicorn_web/tests/unit/conftest.py +++ b/unicorn_web/tests/unit/conftest.py @@ -1,12 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os -import json import boto3 +from aws_lambda_powertools.utilities.typing import LambdaContext + import pytest -from moto import mock_dynamodb, mock_events +from moto import mock_dynamodb, mock_events, mock_sqs @pytest.fixture(scope='function') @@ -18,6 +18,18 @@ def aws_credentials(): os.environ['AWS_SESSION_TOKEN'] = 'testing' +@pytest.fixture(scope='function') +def env_vars(): + os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' + os.environ['SERVICE_NAMESPACE']='unicorn.contracts' + os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' + os.environ['POWERTOOLS_TRACE_DISABLED']='true' + os.environ['POWERTOOLS_LOGGER_LOG_EVENT']='Info' + os.environ['POWERTOOLS_LOGGER_SAMPLE_RATE']='0.1' + os.environ['POWERTOOLS_METRICS_NAMESPACE']='unicorn.contracts' + os.environ['LOG_LEVEL']='INFO' + + @pytest.fixture(scope='function') def dynamodb(aws_credentials): with mock_dynamodb(): @@ -28,3 +40,24 @@ def dynamodb(aws_credentials): def eventbridge(aws_credentials): with mock_events(): yield boto3.client('events', region_name='ap-southeast-2') + + +@pytest.fixture(scope='function') +def sqs(aws_credentials): + with mock_sqs(): + yield boto3.client('sqs', region_name='ap-southeast-2') + + +@pytest.fixture(scope='function') +def lambda_context(): + context: LambdaContext = LambdaContext() + context._function_name="contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._function_version="$LATEST" + context._invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._memory_limit_in_mb=128 + context._aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" + context._log_group_name="/aws/lambda/contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" + # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) + # context._client_context=None + return context diff --git a/unicorn_web/tests/unit/event_generator.py b/unicorn_web/tests/unit/event_generator.py new file mode 100644 index 0000000..334380c --- /dev/null +++ b/unicorn_web/tests/unit/event_generator.py @@ -0,0 +1,177 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +from typing import Any, List + +import json +import hashlib +import uuid +import base64 +import random +import time + +from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent, SQSEvent + + +def apigw_event(http_method: str, + resource: str, + body: Any, + b64encode: bool = False, + path: str = '', + stage: str = 'Local' + ) -> APIGatewayProxyEvent: + body_str = json.dumps(body) + if b64encode: + body_str = base64.b64encode(body_str.encode('utf-8')) + + return APIGatewayProxyEvent({ + "body": body_str, + "resource": resource, + "path": f'/{path}', + "httpMethod": http_method, + "isBase64Encoded": b64encode, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": f"/{path}" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ], + "Accept-Encoding": [ "gzip, deflate, sdch" ], + "Accept-Language": [ "en-US,en;q=0.8" ], + "Cache-Control": [ "max-age=0" ], + "CloudFront-Forwarded-Proto": [ "https" ], + "CloudFront-Is-Desktop-Viewer": [ "true" ], + "CloudFront-Is-Mobile-Viewer": [ "false" ], + "CloudFront-Is-SmartTV-Viewer": [ "false" ], + "CloudFront-Is-Tablet-Viewer": [ "false" ], + "CloudFront-Viewer-Country": [ "US" ], + "Host": [ "0123456789.execute-api.us-east-1.amazonaws.com" ], + "Upgrade-Insecure-Requests": [ "1" ], + "User-Agent": [ "Custom User Agent String" ], + "Via": [ "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" ], + "X-Amz-Cf-Id": [ "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" ], + "X-Forwarded-For": [ "127.0.0.1, 127.0.0.2" ], + "X-Forwarded-Port": [ "443" ], + "X-Forwarded-Proto": [ "https" ], + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": stage, + "requestId": str(uuid.uuid4()), + "requestTime": time.strftime("%d/%b/%Y:%H:%M:%S %z", time.gmtime()), + "requestTimeEpoch": int(time.time()), + "identity": { + "cognitoIdentityPoolId": None, + "accountId": None, + "cognitoIdentityId": None, + "caller": None, + "accessKey": None, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": None, + "userAgent": "Custom User Agent String", + "user": None, + }, + "path": f"/{stage}/{path}", + "resourcePath": resource, + "httpMethod": http_method, + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + }) + + +def sqs_event(messages: List[dict], + queue_name: str = 'MyQueue', + account_id: int = random.randint(100000000000,999999999999), + aws_region: str = 'us-east-1' + ) -> SQSEvent: + + records = [] + for message in messages: + body = json.dumps(message.get('body', '')) + md5ofbody = hashlib.md5(body.encode('utf-8')).hexdigest() + rcv_timestamp = int(time.time() + (random.randint(0, 500)/1000)) # Random delay of 0-500ms + + msg_attributes = dict() + for attr, val in message.get('attributes', dict()).items(): + msg_attributes[attr] = { + "dataType": "String", + "stringValue": val, + "stringListValues": [], + "binaryListValues": [], + } + + records.append({ + "messageId": str(uuid.uuid4()), + "receiptHandle": "MessageReceiptHandle", + "body": body, + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": f"{int(time.time())}", + "SenderId": f"{account_id}", + "ApproximateFirstReceiveTimestamp": str(rcv_timestamp), + "AWSTraceHeader": "Root=1-64ed8007-277749e74aefce547c22fb79;Parent=11d035ab3958d16e;Sampled=1", + }, + "messageAttributes": msg_attributes, + "md5OfBody": md5ofbody, + "eventSource": "aws:sqs", + "eventSourceARN": f"arn:aws:sqs:{aws_region}:{account_id}:{queue_name}", + "awsRegion": aws_region, + }) + + return SQSEvent({ "Records": records }) + + + + +# { +# "Records": [ +# { +# "messageAttributes": { +# "HttpMethod": { +# "stringValue": "PUT", +# "stringListValues": [], +# "binaryListValues": [], +# "dataType": "String" +# } +# }, +# "md5OfMessageAttributes": "39c36267fdf9c8d354b1069e44662d24", +# "md5OfBody": "4bc398b7ae8e52d7f7eaed3cb76c12ef", +# "eventSource": "aws:sqs", +# "eventSourceARN": "arn:aws:sqs:ap-southeast-2:718758479978:uni-prop-local-contract-UnicornContractsIngestQueue-p5zisiK0Xbxn", +# "awsRegion": "ap-southeast-2" +# } +# ] +# } \ No newline at end of file diff --git a/unicorn_web/tests/unit/events/request_approval_event.json b/unicorn_web/tests/unit/events/request_approval_event.json index 23a78cf..b960967 100644 --- a/unicorn_web/tests/unit/events/request_approval_event.json +++ b/unicorn_web/tests/unit/events/request_approval_event.json @@ -1,81 +1,3 @@ { - "resource": "/request_approval", - "path": "/request_approval", - "body": "{\n \"property_id\": \"usa/anytown/main-street/123\"\n}", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Cache-Control": "no-cache", - "Content-Type": "application/json", - "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", - "User-Agent": "PyTest", - "X-Forwarded-For": "203.0.113.123", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Cache-Control": [ - "no-cache" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "test_api_id.execute-api.ap-southeast-1.amazonaws.com" - ], - "User-Agent": [ - "PyTest" - ], - "X-Forwarded-For": [ - "203.0.113.123" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": null, - "multiValueQueryStringParameters": null, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "resourceId": "resource", - "resourcePath": "/request_approval", - "httpMethod": "POST", - "extendedRequestId": "testReqId=", - "requestTime": "01/Dec/2022:01:01:01 +0000", - "path": "/Local/request_approval", - "accountId": "111122223333", - "protocol": "HTTP/1.1", - "stage": "Local", - "domainPrefix": "test_api_id", - "requestTimeEpoch": 1669856461000, - "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "sourceIp": "203.0.113.123", - "principalOrgId": null, - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PyTest", - "user": null - }, - "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", - "apiId": "test_api_id" - }, - "isBase64Encoded": false + "property_id": "usa/anytown/main-street/123" } \ No newline at end of file diff --git a/unicorn_web/tests/unit/helper.py b/unicorn_web/tests/unit/helper.py index 5dfb739..a195a6b 100644 --- a/unicorn_web/tests/unit/helper.py +++ b/unicorn_web/tests/unit/helper.py @@ -1,34 +1,39 @@ -import os +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 import json -import inspect +from pathlib import Path TABLE_NAME = 'table1' EVENTBUS_NAME = 'test-eventbridge' +SQS_QUEUE_NAME = 'test_sqs' +EVENTS_DIR = Path(__file__).parent / 'events' def load_event(filename) -> dict: - file_dir = os.path.dirname(os.path.abspath((inspect.stack()[0])[1])) - print(file_dir) - with open(os.path.join(file_dir, filename), 'r') as f: - return json.load(f) + return json.load(open(EVENTS_DIR / f'{filename}.json', 'r')) def return_env_vars_dict(k={}): - d = { + if k is None: + k = {} + + env_dict = { + "AWS_DEFAULT_REGION": "ap-southeast-2", "DYNAMODB_TABLE": TABLE_NAME, "EVENT_BUS": "test-eventbridge", - "AWS_DEFAULT_REGION": "ap-southeast-2", - "SERVICE_NAMESPACE":"unicorn.web", - "POWERTOOLS_SERVICE_NAME":"unicorn.web", - "POWERTOOLS_TRACE_DISABLED":"true", + "LOG_LEVEL":"INFO", "POWERTOOLS_LOGGER_LOG_EVENT":"true", "POWERTOOLS_LOGGER_SAMPLE_RATE":"0.1", "POWERTOOLS_METRICS_NAMESPACE":"unicorn.web", - "LOG_LEVEL":"INFO" + "POWERTOOLS_SERVICE_NAME":"unicorn.web", + "POWERTOOLS_TRACE_DISABLED":"true", + "SERVICE_NAMESPACE":"unicorn.web", } - d.update(k) - return d + + env_dict |= k + + return env_dict def create_ddb_table_property_web(dynamodb): @@ -107,6 +112,22 @@ def create_ddb_table_property_web(dynamodb): }) return table + def create_test_eventbridge_bus(eventbridge): bus = eventbridge.create_event_bus(Name=EVENTBUS_NAME) return bus + + +def create_test_sqs_ingestion_queue(sqs): + queue = sqs.create_queue(QueueName=SQS_QUEUE_NAME) + return queue + + +def prop_id_to_pk_sk(property_id: str) -> dict[str, str]: + country, city, street, number = property_id.split('/') + pk_details = f"{country}#{city}".replace(' ', '-').lower() + + return { + 'PK': f"PROPERTY#{pk_details}", + 'SK': f"{street}#{str(number)}".replace(' ', '-').lower(), + } diff --git a/unicorn_web/tests/unit/lambda_context.py b/unicorn_web/tests/unit/lambda_context.py deleted file mode 100644 index 4fecd56..0000000 --- a/unicorn_web/tests/unit/lambda_context.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -""" -Simple Lambda Context class to be passed to the lambda handler when test is invoked -""" - - -class LambdaContext: - aws_request_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - log_group_name="/aws/lambda/test_log_group_name" - log_stream_name="2022/12/01/[$LATEST]aaaaaaaabbbbbbbbccccccccdddddddd" - function_name="test_function_name" - memory_limit_in_mb=128 - function_version="$LATEST" - invoked_function_arn="arn:aws:lambda:ap-southeast-2:111111111111:function:test_function_name" - client_context=None - #identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) diff --git a/unicorn_web/tests/unit/test_publication_approved_event_handler.py b/unicorn_web/tests/unit/test_publication_approved_event_handler.py index a38ba87..7281540 100644 --- a/unicorn_web/tests/unit/test_publication_approved_event_handler.py +++ b/unicorn_web/tests/unit/test_publication_approved_event_handler.py @@ -1,36 +1,36 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os +# import os -from unittest import mock -from importlib import reload +# from unittest import mock +# from importlib import reload -from .lambda_context import LambdaContext -from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web +# from .lambda_context import LambdaContext +# from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web -def get_property_pk_sk(property_id): - country, city, street, number = property_id.split('/') - pk_details = f"{country}#{city}".replace(' ', '-').lower() - return { - 'PK': f"PROPERTY#{pk_details}", - 'SK': f"{street}#{str(number)}".replace(' ', '-').lower(), - } +# def get_property_pk_sk(property_id): +# country, city, street, number = property_id.split('/') +# pk_details = f"{country}#{city}".replace(' ', '-').lower() +# return { +# 'PK': f"PROPERTY#{pk_details}", +# 'SK': f"{street}#{str(number)}".replace(' ', '-').lower(), +# } -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_property_approved(dynamodb, mocker): - eventbridge_event = load_event('events/property_approved.json') - property_id = eventbridge_event['detail']['property_id'] +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_property_approved(dynamodb, mocker): +# eventbridge_event = load_event('events/property_approved.json') +# property_id = eventbridge_event['detail']['property_id'] - import approvals_service.publication_approved_event_handler as app - reload(app) # Reload is required to prevent function setup reuse from another test +# import approvals_service.publication_approved_event_handler as app +# reload(app) # Reload is required to prevent function setup reuse from another test - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - ret = app.lambda_handler(eventbridge_event, LambdaContext()) # type: ignore - assert ret['result'] == 'Successfully updated property status' +# ret = app.lambda_handler(eventbridge_event, LambdaContext()) # type: ignore +# assert ret['result'] == 'Successfully updated property status' - ddbitem_after = dynamodb.Table('table1').get_item(Key=get_property_pk_sk(property_id)) - assert ddbitem_after['Item']['status'] == 'APPROVED' +# ddbitem_after = dynamodb.Table('table1').get_item(Key=get_property_pk_sk(property_id)) +# assert ddbitem_after['Item']['status'] == 'APPROVED' diff --git a/unicorn_web/tests/unit/test_request_approval_function.py b/unicorn_web/tests/unit/test_request_approval_function.py index 4026b78..d4a603d 100644 --- a/unicorn_web/tests/unit/test_request_approval_function.py +++ b/unicorn_web/tests/unit/test_request_approval_function.py @@ -1,116 +1,135 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os -import json - -from unittest import mock +# import json from importlib import reload -from .lambda_context import LambdaContext -from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web, create_test_eventbridge_bus +# import pytest +from unittest import mock +# from botocore.exceptions import ClientError +from .event_generator import sqs_event +from .helper import TABLE_NAME +from .helper import load_event, return_env_vars_dict +from .helper import create_ddb_table_property_web, create_test_eventbridge_bus, create_test_sqs_ingestion_queue +from .helper import prop_id_to_pk_sk @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_valid_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/request_approval_event.json') +def test_valid_event(dynamodb, eventbridge, sqs, lambda_context): + payload = load_event('request_approval_event') + event = sqs_event([{'body': payload, 'attributes': {'HttpMethod': 'POST'}}]) # Loading function here so that mocking works correctly. - import approvals_service.request_approval_function as app + from approvals_service import request_approval_function # Reload is required to prevent function setup reuse from another test - reload(app) + reload(request_approval_function) create_ddb_table_property_web(dynamodb) create_test_eventbridge_bus(eventbridge) + create_test_sqs_ingestion_queue(sqs) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) + request_approval_function.lambda_handler(event, lambda_context) - assert ret['statusCode'] == 200 - assert 'result' in data.keys() - assert 'Approval Requested' in data['result'] + # 'PK': 'PROPERTY#usa#anytown', + # 'SK': 'main-street#123', + # usa/anytown/main-street/123 + prop_id = prop_id_to_pk_sk(payload['property_id']) + res = dynamodb.Table(TABLE_NAME).get_item(Key=prop_id) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_broken_input_event(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/request_approval_bad_input.json') + assert res['Item']['PK'] == prop_id['PK'] + assert res['Item']['SK'] == prop_id['SK'] - # Loading function here so that mocking works correctly. - import approvals_service.request_approval_function as app + assert res['Item']['city'] == 'Anytown' + assert res['Item']['contract'] == 'sale' + assert res['Item']['country'] == 'USA' + assert res['Item']['description'] == 'Test Description' + assert res['Item']['listprice'] == '200' + assert res['Item']['number'] == '123' + assert res['Item']['status'] == 'PENDING' + assert res['Item']['street'] == 'Main Street' - # Reload is required to prevent function setup reuse from another test - reload(app) - create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_broken_input_event(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/request_approval_bad_input.json') - assert ret['statusCode'] == 400 - assert 'message' in data.keys() - assert 'unable' in data['message'].lower() +# # Loading function here so that mocking works correctly. +# import approvals_service.request_approval_function as app +# # Reload is required to prevent function setup reuse from another test +# reload(app) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_invalid_property_id(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/request_invalid_property_id.json') +# create_ddb_table_property_web(dynamodb) - # Loading function here so that mocking works correctly. - import approvals_service.request_approval_function as app +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - # Reload is required to prevent function setup reuse from another test - reload(app) +# assert ret['statusCode'] == 400 +# assert 'message' in data.keys() +# assert 'unable' in data['message'].lower() - create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_invalid_property_id(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/request_invalid_property_id.json') - assert ret['statusCode'] == 400 - assert 'message' in data.keys() - assert 'invalid' in data['message'].lower() +# # Loading function here so that mocking works correctly. +# import approvals_service.request_approval_function as app +# # Reload is required to prevent function setup reuse from another test +# reload(app) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_already_approved(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/request_already_approved.json') +# create_ddb_table_property_web(dynamodb) - # Loading function here so that mocking works correctly. - import approvals_service.request_approval_function as app +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - # Reload is required to prevent function setup reuse from another test - reload(app) +# assert ret['statusCode'] == 400 +# assert 'message' in data.keys() +# assert 'invalid' in data['message'].lower() - create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_already_approved(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/request_already_approved.json') - assert ret['statusCode'] == 200 - assert 'result' in data.keys() - assert 'already' in data['result'].lower() +# # Loading function here so that mocking works correctly. +# import approvals_service.request_approval_function as app +# # Reload is required to prevent function setup reuse from another test +# reload(app) -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_property_does_not_exist(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/request_non_existent_property.json') +# create_ddb_table_property_web(dynamodb) - # Loading function here so that mocking works correctly. - import approvals_service.request_approval_function as app +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - # Reload is required to prevent function setup reuse from another test - reload(app) +# assert ret['statusCode'] == 200 +# assert 'result' in data.keys() +# assert 'already' in data['result'].lower() - create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_property_does_not_exist(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/request_non_existent_property.json') + +# # Loading function here so that mocking works correctly. +# import approvals_service.request_approval_function as app + +# # Reload is required to prevent function setup reuse from another test +# reload(app) + +# create_ddb_table_property_web(dynamodb) + +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 404 - assert 'message' in data.keys() - assert 'no property found' in data['message'].lower() +# assert ret['statusCode'] == 404 +# assert 'message' in data.keys() +# assert 'no property found' in data['message'].lower() diff --git a/unicorn_web/tests/unit/test_search_function.py b/unicorn_web/tests/unit/test_search_function.py index 4801f98..f64620b 100644 --- a/unicorn_web/tests/unit/test_search_function.py +++ b/unicorn_web/tests/unit/test_search_function.py @@ -1,143 +1,143 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import os -import json +# import os +# import json -from unittest import mock -from importlib import reload +# from unittest import mock +# from importlib import reload -from .lambda_context import LambdaContext -from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web +# from .lambda_context import LambdaContext +# from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_by_street(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_street_event.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_by_street(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_street_event.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 200 - assert type(data) == list - assert len(data) == 1 - item = data[0] - assert item['city'] == 'Anytown' - assert item['number'] == '124' +# assert ret['statusCode'] == 200 +# assert type(data) == list +# assert len(data) == 1 +# item = data[0] +# assert item['city'] == 'Anytown' +# assert item['number'] == '124' -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_by_city(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_city.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_by_city(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_city.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 200 - assert type(data) == list - assert len(data) == 1 - item = data[0] - assert item['city'] == 'Anytown' - assert item['number'] == '124' +# assert ret['statusCode'] == 200 +# assert type(data) == list +# assert len(data) == 1 +# item = data[0] +# assert item['city'] == 'Anytown' +# assert item['number'] == '124' -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_full_address(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_full_address.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_full_address(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_full_address.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 200 - assert data['city'] == 'Anytown' - assert data['number'] == '124' +# assert ret['statusCode'] == 200 +# assert data['city'] == 'Anytown' +# assert data['number'] == '124' -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_full_address_declined(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_full_address_declined.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_full_address_declined(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_full_address_declined.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 404 - assert 'message' in data - assert 'declined' in data['message'].lower() +# assert ret['statusCode'] == 404 +# assert 'message' in data +# assert 'declined' in data['message'].lower() -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_full_address_new(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_full_address_new.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_full_address_new(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_full_address_new.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 404 - assert 'message' in data - assert 'new' in data['message'].lower() +# assert ret['statusCode'] == 404 +# assert 'message' in data +# assert 'new' in data['message'].lower() -@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_search_full_address_not_found(dynamodb, eventbridge, mocker): - apigw_event = load_event('events/search_by_full_address_not_found.json') +# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +# def test_search_full_address_not_found(dynamodb, eventbridge, mocker): +# apigw_event = load_event('events/search_by_full_address_not_found.json') - # Loading function here so that mocking works correctly. - import search_service.property_search_function as app +# # Loading function here so that mocking works correctly. +# import search_service.property_search_function as app - # Reload is required to prevent function setup reuse from another test - reload(app) +# # Reload is required to prevent function setup reuse from another test +# reload(app) - create_ddb_table_property_web(dynamodb) +# create_ddb_table_property_web(dynamodb) - context = LambdaContext() - ret = app.lambda_handler(apigw_event, context) # type: ignore - data = json.loads(ret['body']) +# context = LambdaContext() +# ret = app.lambda_handler(apigw_event, context) # type: ignore +# data = json.loads(ret['body']) - assert ret['statusCode'] == 404 - assert 'message' in data - assert 'not found' in data['message'].lower() +# assert ret['statusCode'] == 404 +# assert 'message' in data +# assert 'not found' in data['message'].lower() From bc1abb53b851eabe318b1192077b06ea8fb14e63 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:24:53 +1000 Subject: [PATCH 16/87] fix: remove curl-test from make test alias --- unicorn_contracts/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_contracts/Makefile b/unicorn_contracts/Makefile index 224aeba..3777ca5 100644 --- a/unicorn_contracts/Makefile +++ b/unicorn_contracts/Makefile @@ -30,7 +30,7 @@ ci_init: #### Tests -test: unit-test integration-test curl-test +test: unit-test integration-test unit-test: poetry run pytest tests/unit/ From cd069f37566418bb9ebd4da3b1429435f0959b0c Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:25:17 +1000 Subject: [PATCH 17/87] chore: remove unused swagger-cli from make build --- unicorn_web/Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/unicorn_web/Makefile b/unicorn_web/Makefile index 7a9ecb5..9465c2f 100644 --- a/unicorn_web/Makefile +++ b/unicorn_web/Makefile @@ -11,7 +11,6 @@ deps: poetry install build: - swagger-cli validate api.yaml sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export -f requirements.txt --without-hashes --output src/approvals_service/requirements.txt From db480c59ca67b22755ea6ab426d7a6a31ae18a83 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:27:52 +1000 Subject: [PATCH 18/87] chore: Update Python dependencies on unicorn_properties --- unicorn_properties/poetry.lock | 64 +++++++++++++++++++++---------- unicorn_properties/pyproject.toml | 1 + 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/unicorn_properties/poetry.lock b/unicorn_properties/poetry.lock index 3c7c431..5e15694 100644 --- a/unicorn_properties/poetry.lock +++ b/unicorn_properties/poetry.lock @@ -1,14 +1,25 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "arnparse" +version = "0.0.2" +description = "Parse ARNs using Python" +optional = false +python-versions = "*" +files = [ + {file = "arnparse-0.0.2-py2.py3-none-any.whl", hash = "sha256:b0906734e4b8f19e39b1e32944c6cd6274b6da90c066a83882ac7a11d27553e0"}, + {file = "arnparse-0.0.2.tar.gz", hash = "sha256:cb87f17200d07121108a9085d4a09cc69a55582647776b9a917b0b1f279db8f8"}, +] + [[package]] name = "aws-lambda-powertools" -version = "2.23.0" +version = "2.23.1" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.0-py3-none-any.whl", hash = "sha256:a7a2a6aefbbc360ffd234ec903017a46680fd8e06e1ce745f90999fa334c2253"}, - {file = "aws_lambda_powertools-2.23.0.tar.gz", hash = "sha256:3942014d610cd9780904f253e8f7aaeb30ae81f9fbb95c253cbaa4837955fe20"}, + {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, + {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, ] [package.dependencies] @@ -18,6 +29,7 @@ typing-extensions = ">=4.6.2,<5.0.0" [package.extras] all = ["aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "pydantic (>=1.8.2,<2.0.0)"] aws-sdk = ["boto3 (>=1.20.32,<2.0.0)"] +datadog = ["datadog-lambda (>=4.77.0,<5.0.0)"] parser = ["pydantic (>=1.8.2,<2.0.0)"] tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] @@ -39,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.32" +version = "1.28.40" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.32-py3-none-any.whl", hash = "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3"}, - {file = "boto3-1.28.32.tar.gz", hash = "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28"}, + {file = "boto3-1.28.40-py3-none-any.whl", hash = "sha256:bd7c760afb195eaeaab907dc6b2c21fa64ddbba3fed4a869e80d820ddbd6cc70"}, + {file = "boto3-1.28.40.tar.gz", hash = "sha256:6ff9a5b815e106656596064d51c9b6ba97a307807baa5f89634384b7d3f7ecc6"}, ] [package.dependencies] -botocore = ">=1.31.32,<1.32.0" +botocore = ">=1.31.40,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -58,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.32" +version = "1.31.40" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.32-py3-none-any.whl", hash = "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c"}, - {file = "botocore-1.31.32.tar.gz", hash = "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664"}, + {file = "botocore-1.31.40-py3-none-any.whl", hash = "sha256:df766969f0d9ef9eda1a9c9946e0e173c10199f37a9e4c92861f11ddb5c9e702"}, + {file = "botocore-1.31.40.tar.gz", hash = "sha256:ce22a82ef8674f49691477d09558992cc87e7331f65c6a5b0da897ab192240ca"}, ] [package.dependencies] @@ -496,13 +508,13 @@ files = [ [[package]] name = "moto" -version = "4.1.15" +version = "4.2.2" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.1.15-py2.py3-none-any.whl", hash = "sha256:3fbcf91090692c30117d275fb34b48a075a6f65d4712ba6c4d004ffab976db46"}, - {file = "moto-4.1.15.tar.gz", hash = "sha256:272236d312457b324c645741ee589924fd61a96b84680dc2e607f8663c563551"}, + {file = "moto-4.2.2-py2.py3-none-any.whl", hash = "sha256:2a9cbcd9da1a66b23f95d62ef91968284445233a606b4de949379395056276fb"}, + {file = "moto-4.2.2.tar.gz", hash = "sha256:ee34c4c3f53900d953180946920c9dba127a483e2ed40e6dbf93d4ae2e760e7c"}, ] [package.dependencies] @@ -555,13 +567,13 @@ files = [ [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -581,13 +593,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, + {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, ] [package.dependencies] @@ -660,6 +672,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -667,8 +680,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -685,6 +705,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -692,6 +713,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -934,4 +956,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "ea89392c5b9832d401f61ec43f03d41d3f2b7efb94932263c007084f872b5aa7" +content-hash = "12fb02108f72c9c276aa75d575815d049503a519c1b2b3a920914592ca539b37" diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index cf125b2..8217667 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -23,6 +23,7 @@ requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" pyyaml = "^6.0.1" +arnparse = "^0.0.2" [build-system] requires = ["poetry-core>=1.0.0"] From bd48fdff5fa72ea9dafea10c68f1d59e22611f83 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:28:23 +1000 Subject: [PATCH 19/87] chore: remove Coverage config, add Integration tests to pyproject in unicorn_properties --- unicorn_properties/pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index 8217667..fd40c54 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -31,9 +31,10 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -vv -W ignore::UserWarning --cov=properties_service --cov-config=.coveragerc --cov-report term --cov-report html" +addopts = "-ra -vv -W ignore::UserWarning" testpaths = [ - "./tests/unit", + "tests/unit", + "tests/integration", ] [tool.ruff] From 36cf11eefaed64dabcaa70ce6a6d47d0b94bfe13 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:29:42 +1000 Subject: [PATCH 20/87] chore: replace samconfig file from TOML to YAML --- unicorn_properties/samconfig.toml | 11 ----------- unicorn_properties/samconfig.yaml | 33 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) delete mode 100644 unicorn_properties/samconfig.toml create mode 100644 unicorn_properties/samconfig.yaml diff --git a/unicorn_properties/samconfig.toml b/unicorn_properties/samconfig.toml deleted file mode 100644 index 52b1a9a..0000000 --- a/unicorn_properties/samconfig.toml +++ /dev/null @@ -1,11 +0,0 @@ -version = 0.1 -[default] -[default.deploy] -[default.deploy.parameters] -disable_rollback = true -stack_name = "uni-prop-local-properties" -s3_prefix = "uni-prop-local-properties" -capabilities = "CAPABILITY_IAM" -parameter_overrides = "Stage=\"Local\"" -resolve_s3 = true -resolve_image_repositories = true \ No newline at end of file diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml new file mode 100644 index 0000000..97797c9 --- /dev/null +++ b/unicorn_properties/samconfig.yaml @@ -0,0 +1,33 @@ +version: 0.1 + +default: + global: + parameters: + stack_name: uni-prop-local-properties + s3_prefix: uni-prop-local-properties + resolve_s3: true + resolve_image_repositories: true + build: + parameters: + cached: true + parallel: true + deploy: + parameters: + disable_rollback: true + confirm_changeset: false + fail_on_empty_changeset: false + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" + validate: + parameters: + lint: true + sync: + parameters: + watch: true + local_start_api: + parameters: + warm_containers: EAGER + local_start_lambda: + parameters: + warm_containers: EAGER From 7398792ac00dd0233da8e5b889810e377a400f27 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 14:30:19 +1000 Subject: [PATCH 21/87] feat: Add EventBridge subscribers and subscription rules to unicorn_properties --- unicorn_properties/samconfig.yaml | 32 +++++++ unicorn_properties/subscriber-policies.yaml | 46 +++++++++ unicorn_properties/subscriptions.yaml | 101 ++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 unicorn_properties/subscriber-policies.yaml create mode 100644 unicorn_properties/subscriptions.yaml diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 97797c9..386c607 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -31,3 +31,35 @@ default: local_start_lambda: parameters: warm_containers: EAGER + +subscribers: + global: + parameters: + stack_name: uni-prop-local-properties-subscribers + s3_prefix: uni-prop-local-properties-subscribers + resolve_s3: true + deploy: + parameters: + template_file: subscriber-policies.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" + +subscriptions: + global: + parameters: + stack_name: uni-prop-local-properties-subscriptions + s3_prefix: uni-prop-local-properties-subscriptions + resolve_s3: true + deploy: + parameters: + template_file: subscriptions.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" diff --git a/unicorn_properties/subscriber-policies.yaml b/unicorn_properties/subscriber-policies.yaml new file mode 100644 index 0000000..0ed61c3 --- /dev/null +++ b/unicorn_properties/subscriber-policies.yaml @@ -0,0 +1,46 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by the Contracts Service. + +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +Resources: + # Update this policy as you get new subscribers by adding their namespace to events:source + CrossServiceCreateRulePolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBus}}" + StatementId: !Sub CreateRulePolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - events:PutRule + - events:DeleteRule + - events:DescribeRule + - events:DisableRule + - events:EnableRule + - events:PutTargets + - events:RemoveTargets + Resource: + - !Sub + - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* + - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBus}}" + Condition: + StringEqualsIfExists: + "events:creatorAccount": "${aws:PrincipalAccount}" + StringEquals: + "events:source": + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + "Null": + "events:source": "false" diff --git a/unicorn_properties/subscriptions.yaml b/unicorn_properties/subscriptions.yaml new file mode 100644 index 0000000..4d70917 --- /dev/null +++ b/unicorn_properties/subscriptions.yaml @@ -0,0 +1,101 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: Defines the subscriptions for + +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +Resources: + ContractStatusChangedSubscriptionRule: + Type: AWS::Events::Rule + Properties: + Name: properties.statuschanged-contract.pubevalcompleted + Description: Constract Status Changed subscription + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBusArn}}" + EventPattern: + source: + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + detail-type: + - ContractStatusChanged + State: ENABLED + Targets: + - Id: SendEventTo + Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + RoleArn: !GetAtt UnicornContractsEventBusToUnicornPropertiesEventBusRole.Arn + + # This IAM role allows EventBridge to assume the permissions necessary to send events + # from the Unicorn Contracts event bus to the Unicorn Properties event bus. + # If the Unicorn Properties service wants receive events from other services, then add another policy + # the arn of the which includes the event bus arn where Unicorn Properties is creating the + # subscription (rule) + UnicornContractsEventBusToUnicornPropertiesEventBusRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: events.amazonaws.com + Policies: + - PolicyName: PutEventsOnUnicornPropertiesEventBus + PolicyDocument: + Statement: + - Effect: Allow + Action: events:PutEvents + Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + + PublicationApprovalRequestedSubscriptionRule: + Type: AWS::Events::Rule + Properties: + Name: properties.pubapprovalwf-web.pubapprovalrequested + Description: Publication evaluation completed subscription + EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + EventPattern: + source: + - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" + detail-type: + - PublicationApprovalRequested + State: ENABLED + Targets: + - Id: SendEventTo + Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + RoleArn: !GetAtt UnicornWebEventBusToUnicornPropertiesEventBusRole.Arn + + # This IAM role allows EventBridge to assume the permissions necessary to send events + # from the Unicorn Web event bus to the Unicorn Properties event bus. + # If the Unicorn Properties service wants receive events from other services, then add another policy + # the arn of the which includes the event bus arn where Unicorn Properties is creating the + # subscription (rule) + UnicornWebEventBusToUnicornPropertiesEventBusRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: events.amazonaws.com + Policies: + - PolicyName: PutEventsOnUnicornPropertiesEventBus + PolicyDocument: + Statement: + - Effect: Allow + Action: events:PutEvents + Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + +Outputs: + ContractStatusChangedSubscription: + Description: Rule ARN for Contract service event subscription + Value: !GetAtt ContractStatusChangedSubscriptionRule.Arn + + PublicationApprovalRequestedSubscription: + Description: Rule ARN for Web service event subscription + Value: !GetAtt PublicationApprovalRequestedSubscriptionRule.Arn From 7036ed40df05db84fd60f153eb6d82f05649984c Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 15:25:46 +1000 Subject: [PATCH 22/87] chore: move state machine definition out of src/ to match across runtimes --- .../{src => }/state_machine/property_approval.asl.yaml | 2 +- unicorn_properties/template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename unicorn_properties/{src => }/state_machine/property_approval.asl.yaml (99%) diff --git a/unicorn_properties/src/state_machine/property_approval.asl.yaml b/unicorn_properties/state_machine/property_approval.asl.yaml similarity index 99% rename from unicorn_properties/src/state_machine/property_approval.asl.yaml rename to unicorn_properties/state_machine/property_approval.asl.yaml index 1d348b0..4adac6b 100644 --- a/unicorn_properties/src/state_machine/property_approval.asl.yaml +++ b/unicorn_properties/state_machine/property_approval.asl.yaml @@ -20,7 +20,7 @@ States: - ContractStatusNotFoundException Next: NotFound Comment: >- - ContractExistsChecker checks to see if a contract for a psecified + ContractExistsChecker checks to see if a contract for a specified property exists. NotFound: diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index a84118c..840d75e 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -220,7 +220,7 @@ Resources: Type: AWS::Serverless::StateMachine Properties: Name: !Sub "${AWS::StackName}-ApprovalStateMachine" - DefinitionUri: src/state_machine/property_approval.asl.yaml + DefinitionUri: state_machine/property_approval.asl.yaml Tracing: Enabled: true Policies: From 1015c1c8223553e803a5b8e47db66388aad7abed Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 15:35:22 +1000 Subject: [PATCH 23/87] chore: update template section name comments --- unicorn_properties/template.yaml | 52 +++++++++----------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 840d75e..2c54be8 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -6,9 +6,7 @@ Description: > Unicorn Properties Service - events synchronization with Contract Service + Property Approval Workflow -###################################### -# METADATA -###################################### +#### METADATA Metadata: cfn-lint: config: @@ -19,9 +17,7 @@ Metadata: - I3011 # Required to ignore retention policy of inline-created SQS queue for ApprovalStateMachine's EventBridge Rule - I3013 # Required to ignore retention policy of inline-created SQS queue for ApprovalStateMachine's EventBridge Rule -###################################### -# PARAMETERS -###################################### +#### PARAMETERS Parameters: Stage: Type: String @@ -31,9 +27,7 @@ Parameters: - Dev - Prod -###################################### -# MAPPINGS -###################################### +#### MAPPINGS Mappings: LogsRetentionPeriodMap: Local: @@ -43,18 +37,13 @@ Mappings: Prod: Days: 14 -###################################### -# CONDITIONS -###################################### +#### CONDITIONS Conditions: IsProd: !Equals - !Ref Stage - Prod -###################################### -# GLOBALS -# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst -###################################### +#### GLOBALS Globals: Function: Runtime: python3.11 @@ -79,13 +68,9 @@ Globals: project: AWS Serverless Developer Experience service: Unicorn Properties Service -###################################### -# RESOURCES -###################################### +#### RESOURCES Resources: - ###################################### - # LAMBDA FUNCTIONS - ###################################### + #### SSM PARAMETERS ContractStatusChangedHandlerFunction: Type: AWS::Serverless::Function Properties: @@ -180,9 +165,8 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable - ###################################### - # DLQs - ###################################### + + #### DLQs PropertiesEventBusRuleDLQ: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete @@ -213,9 +197,8 @@ Resources: - Key: stage Value: !Ref Stage - ###################################### - # STATE MACHINE - ###################################### + + #### STATE MACHINE ApprovalStateMachine: Type: AWS::Serverless::StateMachine Properties: @@ -281,9 +264,8 @@ Resources: EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" ServiceName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - ###################################### - # CLOUDWATCH LOG GROUPS - ###################################### + + #### CLOUDWATCH LOG GROUPS ContractStatusChangedHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -350,9 +332,7 @@ Resources: - !Ref Stage - Days - ###################################### - # DYNAMODB TABLE - ###################################### + #### DYNAMODB TABLE ContractStatusTable: Type: AWS::DynamoDB::Table UpdateReplacePolicy: Delete @@ -375,9 +355,7 @@ Resources: - Key: stage Value: !Ref Stage -###################################### -# OUTPUTS -###################################### +##### OUTPUTS Outputs: ContractStatusTableName: Value: !Ref ContractStatusTable From 08239a69a4ed89dca36d1f29471131192bc8a40c Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:47:30 +1000 Subject: [PATCH 24/87] chore: update cfn-lint ignored rules --- unicorn_properties/template.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 2c54be8..ca74681 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -11,11 +11,10 @@ Metadata: cfn-lint: config: ignore_checks: + - ES4000 - ES6000 - - ES2003 - - I3042 - - I3011 # Required to ignore retention policy of inline-created SQS queue for ApprovalStateMachine's EventBridge Rule - - I3013 # Required to ignore retention policy of inline-created SQS queue for ApprovalStateMachine's EventBridge Rule + - I3011 + - I3013 #### PARAMETERS Parameters: From 7b17abbb1ebf4b9be296292769d40206ce9a52ff Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:49:15 +1000 Subject: [PATCH 25/87] chore: linted/inlined intrinsic functions for better readability --- unicorn_properties/template.yaml | 41 +++++++------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index ca74681..97907d7 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -38,9 +38,7 @@ Mappings: #### CONDITIONS Conditions: - IsProd: !Equals - - !Ref Stage - - Prod + IsProd: !Equals [!Ref Stage, Prod] #### GLOBALS Globals: @@ -110,11 +108,8 @@ Resources: Policies: - DynamoDBStreamReadPolicy: TableName: !Ref ContractStatusTable - StreamName: !Select - - 3 - - !Split - - / - - !GetAtt ContractStatusTable.StreamArn + StreamName: + !Select [3, !Split ["/", !GetAtt ContractStatusTable.StreamArn]] - SQSSendMessagePolicy: QueueName: !GetAtt PropertiesServiceDLQ.QueueName - Statement: @@ -271,10 +266,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${ContractStatusChangedHandlerFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] PropertiesApprovalSyncFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -282,10 +274,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${PropertiesApprovalSyncFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] ContractExistsCheckerFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -293,10 +282,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${ContractExistsCheckerFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] ContentIntegrityValidatorFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -304,10 +290,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${ContentIntegrityValidatorFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] WaitForContractApprovalFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -315,10 +298,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${WaitForContractApprovalFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] ApprovalStateMachineLogGroup: Type: AWS::Logs::LogGroup @@ -326,10 +306,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/states/${AWS::StackName}-ApprovalStateMachine" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### DYNAMODB TABLE ContractStatusTable: From d51c0fa3791df8414311d66b05e3a7e0a43d991a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:51:29 +1000 Subject: [PATCH 26/87] feat: add local EB bus to unicorn_properties, replace hard-coded service namespace with mapped constant --- unicorn_properties/template.yaml | 139 +++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 97907d7..3aef4f0 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -35,6 +35,11 @@ Mappings: Days: 3 Prod: Days: 14 + Constants: + ServiceNamespace: + Value: "unicorn.properties" + ProjectName: + Value: "AWS Serverless Developer Experience" #### CONDITIONS Conditions: @@ -52,18 +57,18 @@ Globals: Environment: Variables: CONTRACT_STATUS_TABLE: !Ref ContractStatusTable - EVENT_BUS: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" - SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default - POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default - POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + EVENT_BUS: !Ref UnicornPropertiesEventBus + SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default + POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default + POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default + POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage - project: AWS Serverless Developer Experience - service: Unicorn Properties Service + project: !FindInMap [Constants, ProjectName, Value] + namespace: !FindInMap [Constants, ServiceNamespace, Value] #### RESOURCES Resources: @@ -83,7 +88,7 @@ Resources: Type: EventBridgeRule Properties: RuleName: properties.contstatuschangedhdr-contracts.contstatuschanged - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" + EventBusName: !GetAtt UnicornPropertiesEventBus.Name Pattern: source: - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" @@ -170,9 +175,9 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: AWS Serverless Developer Experience - - Key: service - Value: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage Value: !Ref Stage @@ -185,9 +190,9 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: AWS Serverless Developer Experience - - Key: service - Value: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage Value: !Ref Stage @@ -213,7 +218,7 @@ Resources: - ComprehendBasicAccessPolicy: {} - RekognitionDetectOnlyPolicy: {} - EventBridgePutEventsPolicy: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" + EventBusName: !GetAtt UnicornPropertiesEventBus.Name - Statement: - Effect: Allow Action: @@ -238,7 +243,7 @@ Resources: Type: EventBridgeRule Properties: RuleName: properties.pubapprovalwf-web.pubapprovalrequested - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" + EventBusName: !GetAtt UnicornPropertiesEventBus.Name Pattern: source: - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" @@ -255,9 +260,8 @@ Resources: WaitForContractApproval: !GetAtt WaitForContractApprovalFunction.Arn ContentIntegrityValidator: !GetAtt ContentIntegrityValidatorFunction.Arn ImageUploadBucketName: !Sub "{{resolve:ssm:/UniProp/${Stage}/ImagesBucket}}" - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/EventBusName}}" - ServiceName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" - + EventBusName: !GetAtt UnicornPropertiesEventBus.Name + ServiceName: !FindInMap [Constants, ServiceNamespace, Value] #### CLOUDWATCH LOG GROUPS ContractStatusChangedHandlerFunctionLogGroup: @@ -325,14 +329,99 @@ Resources: BillingMode: PAY_PER_REQUEST Tags: - Key: project - Value: AWS Serverless Developer Experience - - Key: service - Value: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + Value: !FindInMap [Constants, ProjectName, Value] + - Key: namespace + Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage Value: !Ref Stage + #### EVENT BUS + UnicornPropertiesEventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Sub UnicornPropertiesBus-${Stage} + + # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) + UnicornPropertiesEventsBusPublishPolicy: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Ref UnicornPropertiesEventBus + StatementId: !Sub PropertiesPublishEventsPolicy-${Stage} + Statement: + Effect: Allow + Principal: + AWS: + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: events:PutEvents + Resource: !GetAtt UnicornPropertiesEventBus.Arn + Condition: + StringEquals: + events:source: + - !FindInMap [Constants, ServiceNamespace, Value] + + # Catchall rule used for development purposes. + UnicornPropertiesCatchAllRule: + Type: AWS::Events::Rule + Properties: + Name: contracts.catchall + Description: Catch all events published by the contracts service. + EventBusName: !Ref UnicornPropertiesEventBus + EventPattern: + account: + - !Ref AWS::AccountId + source: + - !FindInMap [Constants, ServiceNamespace, Value] + State: ENABLED #You may want to disable this rule in production + Targets: + - Arn: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn + Id: !Sub UnicornPropertiesCatchAllLogGroupTarget-${Stage} + + # CloudWatch log group used to catch all events + UnicornPropertiesCatchAllLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub + - "/aws/events/${Stage}/${NS}-catchall" + - Stage: !Ref Stage + NS: !FindInMap [Constants, ServiceNamespace, Value] + RetentionInDays: 3 + + # Permissions to allow EventBridge to send logs to CloudWatch + EventBridgeCloudWatchLogGroupPolicy: + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub EvBToCWLogs-${AWS::StackName} + # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "delivery.logs.amazonaws.com", + "events.amazonaws.com" + ] + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "${UnicornPropertiesCatchAllLogGroup.Arn}" + ] + } + ] + } + ##### OUTPUTS Outputs: + UnicornPropertiesEventBusName: + Value: !GetAtt UnicornPropertiesEventBus.Name + ContractStatusTableName: Value: !Ref ContractStatusTable From 855a317f590078f6c2a668852b5e473d24d9fe9d Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:52:05 +1000 Subject: [PATCH 27/87] feat: add EB SSM Params to unicorn_properties --- unicorn_properties/template.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 3aef4f0..fb68c8d 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -73,6 +73,30 @@ Globals: #### RESOURCES Resources: #### SSM PARAMETERS + # Services own and share their namespace + UnicornPropertiesNamespaceParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornPropertiesNamespace + Value: !FindInMap [Constants, ServiceNamespace, Value] + + # Services share their event bus name and arn + UnicornPropertiesEventBusNameParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBus + Value: !GetAtt UnicornPropertiesEventBus.Name + + UnicornPropertiesEventBusArnParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBusArn + Value: !GetAtt UnicornPropertiesEventBus.Arn + + #### LAMBDA FUNCTIONS ContractStatusChangedHandlerFunction: Type: AWS::Serverless::Function Properties: From 0dfce33fce4ed1193972d937179d75096a3ddaeb Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:52:55 +1000 Subject: [PATCH 28/87] chore: add missing DDB permissions, remove unused SQS permission from unicorn_properties lambda functions --- unicorn_properties/template.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index fb68c8d..b610d07 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -105,8 +105,8 @@ Resources: Policies: - DynamoDBWritePolicy: TableName: !Ref ContractStatusTable - - SQSSendMessagePolicy: - QueueName: !GetAtt PropertiesServiceDLQ.QueueName + - DynamoDBReadPolicy: + TableName: !Ref ContractStatusTable Events: StatusChanged: Type: EventBridgeRule @@ -135,6 +135,8 @@ Resources: CodeUri: src/ Handler: properties_service.properties_approval_sync_function.lambda_handler Policies: + - DynamoDBReadPolicy: + TableName: !Ref ContractStatusTable - DynamoDBStreamReadPolicy: TableName: !Ref ContractStatusTable StreamName: From 70320f6bf5bdf4449bfceae76fd95d69985d11c0 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:53:39 +1000 Subject: [PATCH 29/87] chore: lint unicorn_properties template, change EB trigger rule for ContractStatusChangedFunction --- unicorn_properties/template.yaml | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index b610d07..bc775db 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -108,7 +108,7 @@ Resources: - DynamoDBReadPolicy: TableName: !Ref ContractStatusTable Events: - StatusChanged: + StatusChangedEvent: Type: EventBridgeRule Properties: RuleName: properties.contstatuschangedhdr-contracts.contstatuschanged @@ -190,13 +190,12 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable - #### DLQs PropertiesEventBusRuleDLQ: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete DeletionPolicy: Delete - Properties: + Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: @@ -222,7 +221,6 @@ Resources: - Key: stage Value: !Ref Stage - #### STATE MACHINE ApprovalStateMachine: Type: AWS::Serverless::StateMachine @@ -248,15 +246,15 @@ Resources: - Statement: - Effect: Allow Action: - - "logs:CreateLogDelivery" - - "logs:GetLogDelivery" - - "logs:UpdateLogDelivery" - - "logs:DeleteLogDelivery" - - "logs:ListLogDeliveries" - - "logs:PutResourcePolicy" - - "logs:DescribeResourcePolicies" - - "logs:DescribeLogGroups" - - "cloudwatch:PutMetricData" + - logs:CreateLogDelivery + - logs:GetLogDelivery + - logs:UpdateLogDelivery + - logs:DeleteLogDelivery + - logs:ListLogDeliveries + - logs:PutResourcePolicy + - logs:DescribeResourcePolicies + - logs:DescribeLogGroups + - cloudwatch:PutMetricData Resource: "*" Logging: Destinations: From a870ebef7a11e09e2a9e5de8f7fac591801b447f Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:54:28 +1000 Subject: [PATCH 30/87] chore: fix EventBus name, lint JSON files in unicorn_properties eventbridge event payloads --- ...tus_changed_event_contract_1_approved.json | 2 +- ...status_changed_event_contract_1_draft.json | 2 +- ...tus_changed_event_contract_2_approved.json | 2 +- ...status_changed_event_contract_2_draft.json | 2 +- ...ion_approval_requested_event_all_good.json | 14 +++++----- ...ested_event_inappropriate_description.json | 14 +++++----- ..._requested_event_inappropriate_images.json | 14 +++++----- ...requested_event_non_existing_contract.json | 14 +++++----- ...proval_requested_event_pause_workflow.json | 14 +++++----- .../publication_approval_requested_event.json | 2 +- ...ublication_evaluation_completed_event.json | 27 +++++++++---------- ...put_event_property_approval_requested.json | 14 +++++----- 12 files changed, 60 insertions(+), 61 deletions(-) diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json index 38ad7c8..50c9976 100644 --- a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json +++ b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json @@ -5,4 +5,4 @@ "EventBusName": "UnicornPropertiesEventBus-Local", "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"APPROVED\" }" } -] \ No newline at end of file +] diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json index 0c51208..4c1346c 100644 --- a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json +++ b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json @@ -5,4 +5,4 @@ "EventBusName": "UnicornPropertiesEventBus-Local", "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"DRAFT\" }" } -] \ No newline at end of file +] diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json index 0400307..e274637 100644 --- a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json +++ b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json @@ -5,4 +5,4 @@ "EventBusName": "UnicornPropertiesEventBus-Local", "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"APPROVED\" }" } -] \ No newline at end of file +] diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json index 3380bb0..2305970 100644 --- a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json +++ b/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json @@ -5,4 +5,4 @@ "EventBusName": "UnicornPropertiesEventBus-Local", "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"DRAFT\" }" } -] \ No newline at end of file +] diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json index 5379261..8660eb2 100644 --- a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json +++ b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json @@ -1,8 +1,8 @@ [ - { - "DetailType": "PublicationApprovalRequested", - "Source": "unicorn.properties.web", - "EventBusName": "UnicornPropertiesEventBus-Local", - "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" - } -] \ No newline at end of file + { + "DetailType": "PublicationApprovalRequested", + "Source": "unicorn.web", + "EventBusName": "UnicornPropertiesEventBus-Local", + "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" + } +] diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json index ae52544..2bcbd4e 100644 --- a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json +++ b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json @@ -1,8 +1,8 @@ [ - { - "DetailType": "PublicationApprovalRequested", - "Source": "unicorn.properties.web", - "EventBusName": "UnicornPropertiesEventBus-Local", - "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This is a property for goblins. The property has the worst quality and is atrocious when it comes to design. The property is not clean whatsoever, and will make any property owner have buyers' remorse as soon the property is bought. Keep away from this property as much as possible!\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" - } -] \ No newline at end of file + { + "DetailType": "PublicationApprovalRequested", + "Source": "unicorn.web", + "EventBusName": "UnicornPropertiesEventBus-Local", + "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This is a property for goblins. The property has the worst quality and is atrocious when it comes to design. The property is not clean whatsoever, and will make any property owner have buyers' remorse as soon the property is bought. Keep away from this property as much as possible!\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" + } +] diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json index f1f7fe9..19df291 100644 --- a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json +++ b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json @@ -1,8 +1,8 @@ [ - { - "DetailType": "PublicationApprovalRequested", - "Source": "unicorn.properties.web", - "EventBusName": "UnicornPropertiesEventBus-Local", - "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\",\"prop1_interior4-bad.jpg\"]}" - } -] \ No newline at end of file + { + "DetailType": "PublicationApprovalRequested", + "Source": "unicorn.web", + "EventBusName": "UnicornPropertiesEventBus-Local", + "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\",\"prop1_interior4-bad.jpg\"]}" + } +] diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json index 926cc6b..0126eff 100644 --- a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json +++ b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json @@ -1,8 +1,8 @@ [ - { - "DetailType": "PublicationApprovalRequested", - "Source": "unicorn.properties.web", - "EventBusName": "UnicornPropertiesEventBus-Local", - "Detail": "{\"property_id\":\"usa/anytown/main-street/333\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":333},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" - } -] \ No newline at end of file + { + "DetailType": "PublicationApprovalRequested", + "Source": "unicorn.web", + "EventBusName": "UnicornPropertiesEventBus-Local", + "Detail": "{\"property_id\":\"usa/anytown/main-street/333\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":333},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" + } +] diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json index a6c8fac..09d9c88 100644 --- a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json +++ b/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json @@ -1,8 +1,8 @@ [ - { - "DetailType": "PublicationApprovalRequested", - "Source": "unicorn.properties.web", - "EventBusName": "UnicornPropertiesEventBus-Local", - "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" - } -] \ No newline at end of file + { + "DetailType": "PublicationApprovalRequested", + "Source": "unicorn.web", + "EventBusName": "UnicornPropertiesEventBus-Local", + "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" + } +] diff --git a/unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json b/unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json index 0b008aa..c90cfa7 100644 --- a/unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json +++ b/unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json @@ -2,7 +2,7 @@ "version": "0", "id": "f849f683-76e1-1c84-669d-544a9828dfef", "detail-type": "PublicationApprovalRequested", - "source": "unicorn.properties.web", + "source": "unicorn.web", "account": "123456789012", "time": "2022-08-16T06:33:05Z", "region": "ap-southeast-2", diff --git a/unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json b/unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json index 5992f22..b96c3a6 100644 --- a/unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json +++ b/unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json @@ -1,16 +1,15 @@ { - "version": "0", - "id": "f849f683-76e1-1c84-669d-544a9828dfef", - "detail-type": "PublicationEvaluationCompleted", - "source": "unicorn.properties", - "account": "123456789012", - "time": "2022-08-16T06:33:05Z", - "region": "ap-southeast-2", - "resources": [], - "detail": { - "property_id": "usa/anytown/main-street/111", - "evaluation_result": "APPROVED|DECLINED", - "result_reason": "UNSAFE_IMAGE_DETECTED|BAD_SENTIMENT_DETECTED|..." - } + "version": "0", + "id": "f849f683-76e1-1c84-669d-544a9828dfef", + "detail-type": "PublicationEvaluationCompleted", + "source": "unicorn.properties", + "account": "123456789012", + "time": "2022-08-16T06:33:05Z", + "region": "ap-southeast-2", + "resources": [], + "detail": { + "property_id": "usa/anytown/main-street/111", + "evaluation_result": "APPROVED|DECLINED", + "result_reason": "UNSAFE_IMAGE_DETECTED|BAD_SENTIMENT_DETECTED|..." } - \ No newline at end of file +} diff --git a/unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json b/unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json index 0aaee06..f119e4d 100644 --- a/unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json +++ b/unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json @@ -1,8 +1,8 @@ [ - { - "Source": "unicorn.properties.web", - "Detail": "{ \"property_id\": \"usa/anytown/main-street/111\", \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 111, \"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\", \"contract\": \"sale\", \"listprice\": 200, \"currency\": \"SPL\", \"images\": [ \"prop1_exterior1.jpg\", \"prop1_interior1.jpg\", \"prop1_interior2.jpg\", \"prop1_interior3.jpg\", \"prop1_interior4-bad.jpg\" ] }", - "DetailType": "PublicationApprovalRequested", - "EventBusName": "Local-UnicornPropertiesEventBus" - } - ] \ No newline at end of file + { + "Source": "unicorn.web", + "Detail": "{ \"property_id\": \"usa/anytown/main-street/111\", \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 111, \"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\", \"contract\": \"sale\", \"listprice\": 200, \"currency\": \"SPL\", \"images\": [ \"prop1_exterior1.jpg\", \"prop1_interior1.jpg\", \"prop1_interior2.jpg\", \"prop1_interior3.jpg\", \"prop1_interior4-bad.jpg\" ] }", + "DetailType": "PublicationApprovalRequested", + "EventBusName": "UnicornPropertiesEventBus-Local" + } +] From b11afb8ec660acfe0a5e270b4530922753f42ba7 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:55:38 +1000 Subject: [PATCH 31/87] chore: replace hardcoded stack name from Makefile with YQ command to read from samconfig --- unicorn_properties/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index 75f760c..495e0e2 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -1,4 +1,5 @@ -stackName := uni-prop-local-properties +#### Global Variables +stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.yaml) build: cfn-lint template.yaml -a cfn_lint_serverless.rules From 014f2c319b8dd7c698b6ce720b841cc8156ade0a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:58:30 +1000 Subject: [PATCH 32/87] chore: lint and document Makefile stage groups --- unicorn_properties/Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index 495e0e2..dc63038 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -1,6 +1,8 @@ #### Global Variables stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.yaml) + +#### Build/Deploy Tasks build: cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt @@ -12,14 +14,17 @@ deps: deploy: build sam deploy --no-confirm-changeset -sync: - sam sync --stack-name $(stackName) --watch +#### Tests test: unit-test unit-test: poetry run pytest tests/unit/ +#### Utilities +sync: + sam sync --stack-name $(stackName) --watch + logs: sam logs --stack-name $(stackName) -t From fd19ea1a5bb3af5d21643f9fd29bcf39e696d70e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:59:06 +1000 Subject: [PATCH 33/87] test: add Integration tests scaffold --- unicorn_properties/Makefile | 5 +- .../tests/integration/__init__.py | 74 ++++++++++++ .../put_event_contract_status_changed.json | 8 ++ ..._event_publication_approval_requested.json | 8 ++ .../test_contract_status_changed_event.py | 109 ++++++++++++++++++ ...st_publication_approval_requested_event.py | 1 + 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 unicorn_properties/tests/integration/__init__.py create mode 100644 unicorn_properties/tests/integration/events/put_event_contract_status_changed.json create mode 100644 unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json create mode 100644 unicorn_properties/tests/integration/test_contract_status_changed_event.py create mode 100644 unicorn_properties/tests/integration/test_publication_approval_requested_event.py diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index dc63038..e67d11d 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -16,11 +16,14 @@ deploy: build #### Tests -test: unit-test +test: unit-test integration-test unit-test: poetry run pytest tests/unit/ +integration-test: deps + poetry run pytest tests/integration/ + #### Utilities sync: sam sync --stack-name $(stackName) --watch diff --git a/unicorn_properties/tests/integration/__init__.py b/unicorn_properties/tests/integration/__init__.py new file mode 100644 index 0000000..e54c0af --- /dev/null +++ b/unicorn_properties/tests/integration/__init__.py @@ -0,0 +1,74 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +from typing import Iterator + +import json +from pathlib import Path + +import boto3 +from arnparse import arnparse +from yaml import load, Loader + + +#### CONSTANTS +DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' +STACK_OUTPUTS = dict() +EVENTS_DIR = Path(__file__).parent / 'events' + + +#### AWS SDK Objects +cfn = boto3.client('cloudformation') +# cwl = boto3.client('logs') +# ddb = boto3.client('dynamodb') + + +def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: + with open(samconfig, 'r') as f: + conf = load(f, Loader=Loader) + stack_name = conf['default']['global']['parameters']['stack_name'] + + return stack_name + + +def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: + """ + Get the value of an output + """ + + if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): + try: + response = cfn.describe_stacks(StackName=stack_name) + except Exception as e: + raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e + + outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} + STACK_OUTPUTS[stack_name] = outputs + + try: + return outputs[output_name] + except KeyError as e: + raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e + + +def get_event_payload(file) -> dict: + return json.load(open(EVENTS_DIR / f'{file}.json', 'r')) + + +# def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: +# group_name = arnparse(eb_log_group_arn).resource + +# # Get the CW LogStream with the latest log messages +# stream_response = cwl.describe_log_streams(logGroupName=group_name, orderBy='LastEventTime',descending=True,limit=3) +# latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] +# # Fetch log events from that stream +# responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] + +# # Filter log events that match the required `property_id` +# for response in responses: +# for event in response["events"]: +# if (ev := json.loads(event["message"])).get('detail', {}).get('property_id', '') == property_id: +# yield ev + + +# def clean_ddb(table_name, property_id): +# ddb.delete_item(TableName=table_name, Key={ 'property_id': { 'S': property_id } }) diff --git a/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json b/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json new file mode 100644 index 0000000..3d395a1 --- /dev/null +++ b/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json @@ -0,0 +1,8 @@ +[ + { + "Source": "unicorn.contracts", + "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": \"199\",\"property_id\":\"bbb\",\"contract_status\":\"APPROVED\"}", + "DetailType": "ContractStatusChanged", + "EventBusName": "Dev-UnicornPropertiesEventBus" + } +] \ No newline at end of file diff --git a/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json b/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json new file mode 100644 index 0000000..3791334 --- /dev/null +++ b/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json @@ -0,0 +1,8 @@ +[ + { + "EventBusName": "Dev-UnicornPropertiesEventBus", + "Source": "unicorn.properties.web", + "DetailType": "PublicationApprovalRequested", + "Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\": \"sale\",\"listprice\": 200,\"currency\": \"SPL\",\"images\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}" + } +] \ No newline at end of file diff --git a/unicorn_properties/tests/integration/test_contract_status_changed_event.py b/unicorn_properties/tests/integration/test_contract_status_changed_event.py new file mode 100644 index 0000000..c4e9f4b --- /dev/null +++ b/unicorn_properties/tests/integration/test_contract_status_changed_event.py @@ -0,0 +1,109 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +import json +from time import sleep +from datetime import datetime + +import boto3 +from unittest import TestCase + +from . import get_stack_output + + +evb = boto3.client('events') +ddb = boto3.client('dynamodb') + + +properties_bus = get_stack_output('UnicornPropertiesEventBusName') + + +def send_contract_status_changed(property_id, contract_id, contract_status): + return evb.put_events(Entries=[{ + 'Source': 'unicorn.contracts', + 'DetailType': 'ContractStatusChanged', + 'EventBusName': properties_bus, + 'Detail': json.dumps({ + 'contract_last_modified_on': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), + 'property_id': property_id, + 'contract_id': contract_id, + 'contract_status': contract_status}), + }]) + + +def send_publication_approval_requested(property_id, contract_id, contract_status): + return evb.put_events(Entries=[{ + 'Source': 'unicorn.web', + 'DetailType': 'PublicationApprovalRequested', + 'EventBusName': properties_bus, + 'Detail': json.dumps({ + 'contract_last_modified_on': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), + 'property_id': property_id, + 'contract_id': contract_id, + 'contract_status': contract_status}), + }]) + + + +# { +# "DetailType": "PublicationApprovalRequested", +# "Source": "unicorn.properties.web", +# "EventBusName": "UnicornPropertiesEventBus-Local", +# "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" +# } + +# x = { +# "property_id":"usa/anytown/main-street/222", +# "address":{ +# "country":"USA", +# "city":"Anytown", +# "street":"Main Street", +# "number":222 +# }, +# "description":"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", +# "contract":"sale", +# "listprice":200, +# "currency":"SPL", +# "images":[ +# "prop1_exterior1.jpg", +# "prop1_interior1.jpg", +# "prop1_interior2.jpg", +# "prop1_interior3.jpg" +# ] +# } + + + + +class TestCreateContract(TestCase): + properties_bus: str + contract_status_table_name: str + + def setUp(self) -> None: + self.contract_status_table_name = get_stack_output('ContractStatusTableName') + + + def test_contract_status_draft(self): + evp_resp = send_contract_status_changed(property_id='usa/anytown/main-street/111', + contract_id='f2bedc80-3dc8-4544-9140-9b606d71a6ee', + contract_status='DRAFT') + + sleep(2) + + ddb_resp = ddb.get_item(TableName=self.contract_status_table_name, + Key={'property_id': {'S': 'usa/anytown/main-street/111'}}) + + assert ddb_resp['Item']['contract_status']['S'] == 'DRAFT' + assert ddb_resp['Item'].get('sfn_wait_approved_task_token', None) is None + + def test_contract_status_approved(self): + evp_resp = send_contract_status_changed(property_id='usa/anytown/main-street/111', + contract_id='f2bedc80-3dc8-4544-9140-9b606d71a6ee', + contract_status='APPROVED') + + sleep(2) + + ddb_resp = ddb.get_item(TableName=self.contract_status_table_name, + Key={'property_id': {'S': 'usa/anytown/main-street/111'}}) + + assert ddb_resp['Item']['contract_status']['S'] == 'APPROVED' + assert ddb_resp['Item'].get('sfn_wait_approved_task_token', None) is not None diff --git a/unicorn_properties/tests/integration/test_publication_approval_requested_event.py b/unicorn_properties/tests/integration/test_publication_approval_requested_event.py new file mode 100644 index 0000000..336183e --- /dev/null +++ b/unicorn_properties/tests/integration/test_publication_approval_requested_event.py @@ -0,0 +1 @@ +# Event triggers SFN From f432c5aaee9c3adfcd6e93a0144ef7f910d626aa Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:59:29 +1000 Subject: [PATCH 34/87] chore: Add SAM validate to Makefile build stage --- unicorn_properties/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index e67d11d..1869a27 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -4,6 +4,7 @@ stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.y #### Build/Deploy Tasks build: + sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt sam build -c $(DOCKER_OPTS) From 53c8fd46f67d4bb86b19e0f9117b95d238de08d2 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 16:59:58 +1000 Subject: [PATCH 35/87] chore: remove unecessary SAM command line argument from build stage - settings are enforced in samconfig.yaml --- unicorn_properties/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index 1869a27..1bb920b 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -13,7 +13,7 @@ deps: poetry install deploy: build - sam deploy --no-confirm-changeset + sam deploy #### Tests From fbab0f858fc077ff35bda54742dc00a5eb9c8406 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 17:01:20 +1000 Subject: [PATCH 36/87] chore: lint JSON files, remove unused JSON event payloads --- .../dbb_stream_events/contract_status_changed_draft.json | 2 +- .../tests/events/dbb_stream_events/sfn_check_exists.json | 2 +- .../tests/events/put_event_contract_status_changed.json | 8 -------- .../events/put_event_publication_approval_requested.json | 8 -------- 4 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 unicorn_properties/tests/events/put_event_contract_status_changed.json delete mode 100644 unicorn_properties/tests/events/put_event_publication_approval_requested.json diff --git a/unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json b/unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json index 5c8278f..7c01784 100644 --- a/unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json +++ b/unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json @@ -22,4 +22,4 @@ "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" } ] -} \ No newline at end of file +} diff --git a/unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json b/unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json index de4a5ca..c5c84dc 100644 --- a/unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json +++ b/unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json @@ -31,4 +31,4 @@ "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" } ] -} \ No newline at end of file +} diff --git a/unicorn_properties/tests/events/put_event_contract_status_changed.json b/unicorn_properties/tests/events/put_event_contract_status_changed.json deleted file mode 100644 index 3d395a1..0000000 --- a/unicorn_properties/tests/events/put_event_contract_status_changed.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "Source": "unicorn.contracts", - "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": \"199\",\"property_id\":\"bbb\",\"contract_status\":\"APPROVED\"}", - "DetailType": "ContractStatusChanged", - "EventBusName": "Dev-UnicornPropertiesEventBus" - } -] \ No newline at end of file diff --git a/unicorn_properties/tests/events/put_event_publication_approval_requested.json b/unicorn_properties/tests/events/put_event_publication_approval_requested.json deleted file mode 100644 index 3791334..0000000 --- a/unicorn_properties/tests/events/put_event_publication_approval_requested.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "EventBusName": "Dev-UnicornPropertiesEventBus", - "Source": "unicorn.properties.web", - "DetailType": "PublicationApprovalRequested", - "Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\": \"sale\",\"listprice\": 200,\"currency\": \"SPL\",\"images\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}" - } -] \ No newline at end of file From 0857d8514ab0baf391bf2ede81ec93fb75d2107f Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 4 Sep 2023 17:13:52 +1000 Subject: [PATCH 37/87] bug: fix EB catch-all rule pattern match, add naming to EB catch-all CW Logs group, removed unused cfn-lint ignore rule --- unicorn_contracts/template.yaml | 8 +++++++- unicorn_properties/template.yaml | 2 +- unicorn_web/template.yaml | 15 ++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index e8ce3f7..bd6d3df 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -306,8 +306,10 @@ Resources: Description: Catch all events published by the contracts service. EventBusName: !Ref UnicornContractsEventBus EventPattern: + account: + - !Ref AWS::AccountId source: - - prefix: "" + - !FindInMap [Constants, ServiceNamespace, Value] State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn @@ -319,6 +321,10 @@ Resources: UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: + LogGroupName: !Sub + - "/aws/events/${Stage}/${NS}-catchall" + - Stage: !Ref Stage + NS: !FindInMap [Constants, ServiceNamespace, Value] RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] # Permissions to allow EventBridge to send logs to CloudWatch diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index bc775db..ebf4e13 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -410,7 +410,7 @@ Resources: - "/aws/events/${Stage}/${NS}-catchall" - Stage: !Ref Stage NS: !FindInMap [Constants, ServiceNamespace, Value] - RetentionInDays: 3 + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] # Permissions to allow EventBridge to send logs to CloudWatch EventBridgeCloudWatchLogGroupPolicy: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index a2af72d..6425fea 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -376,18 +376,15 @@ Resources: # Catchall rule used for development purposes. UnicornWebCatchAllRule: Type: AWS::Events::Rule - Metadata: - cfn-lint: - config: - ignore_checks: - - ES4000 Properties: Name: contracts.catchall Description: Catch all events published by the web service. EventBusName: !Ref UnicornWebEventBus EventPattern: + account: + - !Ref AWS::AccountId source: - - prefix: "" + - !FindInMap [Constants, ServiceNamespace, Value] State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornWebCatchAllLogGroup.Arn @@ -399,7 +396,11 @@ Resources: UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - RetentionInDays: 3 + LogGroupName: !Sub + - "/aws/events/${Stage}/${NS}-catchall" + - Stage: !Ref Stage + NS: !FindInMap [Constants, ServiceNamespace, Value] + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] # Permissions to allow EventBridge to send logs to CloudWatch EventBridgeCloudWatchLogGroupPolicy: From 899b8747b215ce2d50a9a36c80fd50d2e105f2a0 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:21:26 +1000 Subject: [PATCH 38/87] feat: Split event schemas and registry to each service --- .../integration/ContractStatusChanged.json | 85 +++++++ .../integration/event-schemas.yaml | 140 +++++++++++ unicorn_contracts/samconfig.yaml | 15 ++ .../integration/event-schemas.yaml | 46 ++++ unicorn_properties/samconfig.yaml | 13 + .../PublicationApprovalRequested.json | 126 ++++++++++ .../PublicationEvaluationCompleted.json | 76 ++++++ .../integration}/event-schemas.yaml | 232 +++++++++++------- unicorn_web/samconfig.yaml | 15 ++ 9 files changed, 664 insertions(+), 84 deletions(-) create mode 100644 unicorn_contracts/integration/ContractStatusChanged.json create mode 100644 unicorn_contracts/integration/event-schemas.yaml create mode 100644 unicorn_properties/integration/event-schemas.yaml create mode 100644 unicorn_web/integration/PublicationApprovalRequested.json create mode 100644 unicorn_web/integration/PublicationEvaluationCompleted.json rename {unicorn_properties/cloudformation => unicorn_web/integration}/event-schemas.yaml (58%) diff --git a/unicorn_contracts/integration/ContractStatusChanged.json b/unicorn_contracts/integration/ContractStatusChanged.json new file mode 100644 index 0000000..9d09b60 --- /dev/null +++ b/unicorn_contracts/integration/ContractStatusChanged.json @@ -0,0 +1,85 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "ContractStatusChanged" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "ContractStatusChanged", + "x-amazon-events-source": "unicorn.contracts", + "properties": { + "detail": { + "$ref": "#/components/schemas/ContractStatusChanged" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "ContractStatusChanged": { + "type": "object", + "required": [ + "contract_last_modified_on", + "contract_id", + "contract_status", + "property_id" + ], + "properties": { + "contract_id": { + "type": "string" + }, + "contract_last_modified_on": { + "type": "string", + "format": "date-time" + }, + "contract_status": { + "type": "string" + }, + "property_id": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml new file mode 100644 index 0000000..3a52b86 --- /dev/null +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -0,0 +1,140 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Event Schemas for use by the Contracts Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES +Resources: + EventRegistry: + Type: AWS::EventSchemas::Registry + Properties: + Description: 'Event schemas for Unicorn Contracts' + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + ContractApprovedEventSchema: + Type: AWS::EventSchemas::Schema + Properties: + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@ContractStatusChanged' + Description: 'The schema for a request to publish a property' + Content: > + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "ContractStatusChanged" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "ContractStatusChanged", + "x-amazon-events-source": "unicorn.contracts", + "properties": { + "detail": { + "$ref": "#/components/schemas/ContractStatusChanged" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "ContractStatusChanged": { + "type": "object", + "required": [ + "contract_last_modified_on", + "contract_id", + "contract_status", + "property_id" + ], + "properties": { + "contract_id": { + "type": "string" + }, + "contract_last_modified_on": { + "type": "string", + "format": "date-time" + }, + "contract_status": { + "type": "string" + }, + "property_id": { + "type": "string" + } + } + } + } + } + } diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 015b728..d25be65 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -32,6 +32,21 @@ default: parameters: warm_containers: EAGER +schema: + global: + parameters: + stack_name: uni-prop-local-contract-schema + s3_prefix: uni-prop-local-contract-schema + resolve_s3: true + deploy: + parameters: + template_file: integration/event-schemas.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml new file mode 100644 index 0000000..ec876bd --- /dev/null +++ b/unicorn_properties/integration/event-schemas.yaml @@ -0,0 +1,46 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Event Schemas for use by the Properties Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES +Resources: + EventRegistry: + Type: AWS::EventSchemas::Registry + Properties: + Description: 'Event schemas for Unicorn Properties' + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 386c607..4fc5c54 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -32,6 +32,19 @@ default: parameters: warm_containers: EAGER +schema: + deploy: + parameters: + stack_name: uni-prop-local-properties-schema + s3_prefix: uni-prop-local-properties-schema + template_file: integration/event-schemas.yaml + resolve_s3: true + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: diff --git a/unicorn_web/integration/PublicationApprovalRequested.json b/unicorn_web/integration/PublicationApprovalRequested.json new file mode 100644 index 0000000..a879a32 --- /dev/null +++ b/unicorn_web/integration/PublicationApprovalRequested.json @@ -0,0 +1,126 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationApprovalRequested" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationApprovalRequested", + "x-amazon-events-source": "unicorn.web", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationApprovalRequested" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationApprovalRequested": { + "type": "object", + "required": [ + "images", + "address", + "listprice", + "contract", + "description", + "currency", + "property_id", + "status" + ], + "properties": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "contract": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "listprice": { + "type": "string" + }, + "property_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "Address": { + "type": "object", + "required": [ + "country", + "number", + "city", + "street" + ], + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "number": { + "type": "string" + }, + "street": { + "type": "string" + } + } + } + } + } +} diff --git a/unicorn_web/integration/PublicationEvaluationCompleted.json b/unicorn_web/integration/PublicationEvaluationCompleted.json new file mode 100644 index 0000000..168ca94 --- /dev/null +++ b/unicorn_web/integration/PublicationEvaluationCompleted.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationEvaluationCompleted" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "unicorn.web", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationEvaluationCompleted" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationEvaluationCompleted": { + "type": "object", + "required": [ + "property_id", + "evaluation_result" + ], + "properties": { + "property_id": { + "type": "string" + }, + "evaluation_result": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/unicorn_properties/cloudformation/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml similarity index 58% rename from unicorn_properties/cloudformation/event-schemas.yaml rename to unicorn_web/integration/event-schemas.yaml index 3c56f43..b99fabe 100644 --- a/unicorn_properties/cloudformation/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -1,40 +1,85 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Metadata: - License: Apache-2.0 -Description: 'Event Schemas for use by the Property Service' +Description: 'Event Schemas for use by the Web Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES Resources: - UnicornPropertiesEventRegistry: - Type: AWS::EventSchemas::Registry - Properties: - Description: 'Event schemas for Unicorn Properties' - RegistryName: 'unicorn.contracts' - UnicornWebEventRegistry: + EventRegistry: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Web' - RegistryName: 'unicorn.web' - ContractApprovedEventSchema: + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + PublicationApprovalRequested: Type: AWS::EventSchemas::Schema Properties: - SchemaName: 'unicorn.contracts@ContractStatusChanged' - Content: ' + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationApprovalRequested' + Description: 'The schema for a request to publish a property' + Content: > { "openapi": "3.0.0", "info": { "version": "1.0.0", - "title": "ContractStatusChanged" + "title": "PublicationApprovalRequested" }, "paths": {}, "components": { "schemas": { "AWSEvent": { "type": "object", - "required": ["detail-type", "resources", "detail", "id", "source", "time", "region", "version", "account"], - "x-amazon-events-detail-type": "ContractStatusChanged", - "x-amazon-events-source": "unicorn.contracts", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationApprovalRequested", + "x-amazon-events-source": "unicorn.web", "properties": { "detail": { - "$ref": "#/components/schemas/ContractStatusChanged" + "$ref": "#/components/schemas/PublicationApprovalRequested" }, "account": { "type": "string" @@ -51,7 +96,7 @@ Resources: "resources": { "type": "array", "items": { - "type": "object" + "type": "string" } }, "source": { @@ -66,53 +111,111 @@ Resources: } } }, - "ContractStatusChanged": { + "PublicationApprovalRequested": { "type": "object", - "required": ["contract_last_modified_on", "contract_id", "contract_status", "property_id"], + "required": [ + "images", + "address", + "listprice", + "contract", + "description", + "currency", + "property_id", + "status" + ], "properties": { - "contract_id": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "contract": { "type": "string" }, - "contract_last_modified_on": { - "type": "string", - "format": "date-time" + "currency": { + "type": "string" }, - "contract_status": { + "description": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "listprice": { "type": "string" }, "property_id": { "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "Address": { + "type": "object", + "required": [ + "country", + "number", + "city", + "street" + ], + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "number": { + "type": "string" + }, + "street": { + "type": "string" } } } } } - }' - Description: 'The schema for a property approval event' - RegistryName: !GetAtt UnicornPropertiesEventRegistry.RegistryName - Type: 'OpenApi3' - PublicationApprovalRequested: + } + + + PublicationEvaluationCompleted: Type: AWS::EventSchemas::Schema Properties: - SchemaName: 'unicorn.web@PublicationApprovalRequested' - Content: ' + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' + Description: 'The schema for when a property evaluation is completed' + Content: > { "openapi": "3.0.0", "info": { "version": "1.0.0", - "title": "PublicationApprovalRequested" + "title": "PublicationEvaluationCompleted" }, "paths": {}, "components": { "schemas": { "AWSEvent": { "type": "object", - "required": ["detail-type", "resources", "detail", "id", "source", "time", "region", "version", "account"], - "x-amazon-events-detail-type": "PublicationApprovalRequested", - "x-amazon-events-source": "unicorn.properties.web", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "unicorn.web", "properties": { "detail": { - "$ref": "#/components/schemas/PublicationApprovalRequested" + "$ref": "#/components/schemas/PublicationEvaluationCompleted" }, "account": { "type": "string" @@ -144,60 +247,21 @@ Resources: } } }, - "PublicationApprovalRequested": { + "PublicationEvaluationCompleted": { "type": "object", - "required": ["images", "address", "listprice", "contract", "description", "currency", "property_id", "status"], + "required": [ + "property_id", + "evaluation_result" + ], "properties": { - "address": { - "$ref": "#/components/schemas/Address" - }, - "contract": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "description": { - "type": "string" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "listprice": { - "type": "string" - }, "property_id": { "type": "string" }, - "status": { - "type": "string" - } - } - }, - "Address": { - "type": "object", - "required": ["country", "number", "city", "street"], - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "number": { - "type": "string" - }, - "street": { + "evaluation_result": { "type": "string" } } } } } - }' - Description: 'The schema for a request to publish a property' - RegistryName: !GetAtt UnicornWebEventRegistry.RegistryName - Type: 'OpenApi3' + } \ No newline at end of file diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index 138b1d9..6eb745f 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -32,6 +32,21 @@ default: parameters: warm_containers: EAGER +schema: + global: + parameters: + stack_name: uni-prop-local-web-schema + s3_prefix: uni-prop-local-web-schema + resolve_s3: true + deploy: + parameters: + template_file: integration/event-schemas.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: From ce1087e76b78ef82f5b3a1129cc3bcfdfc578cc2 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:26:32 +1000 Subject: [PATCH 39/87] chore: Add license header to state_machine definition --- unicorn_properties/state_machine/property_approval.asl.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unicorn_properties/state_machine/property_approval.asl.yaml b/unicorn_properties/state_machine/property_approval.asl.yaml index 4adac6b..4cdff43 100644 --- a/unicorn_properties/state_machine/property_approval.asl.yaml +++ b/unicorn_properties/state_machine/property_approval.asl.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 Comment: >- The property approval workflow ensures that its images and content is safe to publish and that there is an approved contract in place before the listing is From c25e9ddd41029f0a8a6c3163086aa02ae9add4f5 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:33:33 +1000 Subject: [PATCH 40/87] fix: moved schema for PublicationEvaluationCompleted event from unicorn_web to unicorn_properties --- .../integration/event-schemas.yaml | 4 +- .../PublicationEvaluationCompleted.json | 0 .../integration/event-schemas.yaml | 85 ++++++++++++++++++ unicorn_web/integration/event-schemas.yaml | 90 +------------------ 4 files changed, 89 insertions(+), 90 deletions(-) rename {unicorn_web => unicorn_properties}/integration/PublicationEvaluationCompleted.json (100%) diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index 3a52b86..5208b72 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -52,7 +52,7 @@ Resources: RegistryName: !GetAtt EventRegistry.RegistryName SchemaName: !Sub '${EventRegistry.RegistryName}@ContractStatusChanged' Description: 'The schema for a request to publish a property' - Content: > + Content: !Sub | { "openapi": "3.0.0", "info": { @@ -76,7 +76,7 @@ Resources: "account" ], "x-amazon-events-detail-type": "ContractStatusChanged", - "x-amazon-events-source": "unicorn.contracts", + "x-amazon-events-source": "${EventRegistry.RegistryName}", "properties": { "detail": { "$ref": "#/components/schemas/ContractStatusChanged" diff --git a/unicorn_web/integration/PublicationEvaluationCompleted.json b/unicorn_properties/integration/PublicationEvaluationCompleted.json similarity index 100% rename from unicorn_web/integration/PublicationEvaluationCompleted.json rename to unicorn_properties/integration/PublicationEvaluationCompleted.json diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml index ec876bd..05088c2 100644 --- a/unicorn_properties/integration/event-schemas.yaml +++ b/unicorn_properties/integration/event-schemas.yaml @@ -44,3 +44,88 @@ Resources: Resource: - !GetAtt EventRegistry.RegistryArn - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + PublicationEvaluationCompleted: + Type: AWS::EventSchemas::Schema + Properties: + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' + Description: 'The schema for when a property evaluation is completed' + Content: !Sub | + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationEvaluationCompleted" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "${EventRegistry.RegistryName}", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationEvaluationCompleted" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationEvaluationCompleted": { + "type": "object", + "required": [ + "property_id", + "evaluation_result" + ], + "properties": { + "property_id": { + "type": "string" + }, + "evaluation_result": { + "type": "string" + } + } + } + } + } + } diff --git a/unicorn_web/integration/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml index b99fabe..6091b4d 100644 --- a/unicorn_web/integration/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -52,7 +52,7 @@ Resources: RegistryName: !GetAtt EventRegistry.RegistryName SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationApprovalRequested' Description: 'The schema for a request to publish a property' - Content: > + Content: !Sub | { "openapi": "3.0.0", "info": { @@ -76,7 +76,7 @@ Resources: "account" ], "x-amazon-events-detail-type": "PublicationApprovalRequested", - "x-amazon-events-source": "unicorn.web", + "x-amazon-events-source": "${EventRegistry.RegistryName}", "properties": { "detail": { "$ref": "#/components/schemas/PublicationApprovalRequested" @@ -179,89 +179,3 @@ Resources: } } } - - - PublicationEvaluationCompleted: - Type: AWS::EventSchemas::Schema - Properties: - Type: 'OpenApi3' - RegistryName: !GetAtt EventRegistry.RegistryName - SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' - Description: 'The schema for when a property evaluation is completed' - Content: > - { - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "PublicationEvaluationCompleted" - }, - "paths": {}, - "components": { - "schemas": { - "AWSEvent": { - "type": "object", - "required": [ - "detail-type", - "resources", - "detail", - "id", - "source", - "time", - "region", - "version", - "account" - ], - "x-amazon-events-detail-type": "PublicationEvaluationCompleted", - "x-amazon-events-source": "unicorn.web", - "properties": { - "detail": { - "$ref": "#/components/schemas/PublicationEvaluationCompleted" - }, - "account": { - "type": "string" - }, - "detail-type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "region": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "source": { - "type": "string" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" - } - } - }, - "PublicationEvaluationCompleted": { - "type": "object", - "required": [ - "property_id", - "evaluation_result" - ], - "properties": { - "property_id": { - "type": "string" - }, - "evaluation_result": { - "type": "string" - } - } - } - } - } - } \ No newline at end of file From f83e750287f5f701e162567e205e0259b5b1c90b Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Tue, 5 Sep 2023 10:48:38 +0800 Subject: [PATCH 41/87] chore: minor changes to templates --- .gitignore | 4 ++++ unicorn_contracts/template.yaml | 7 +++++-- unicorn_properties/template.yaml | 29 +++++++++++++++++------------ unicorn_web/template.yaml | 16 ++++++++++------ 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100755 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..54d784a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.idea/* +**/.aws-sam/ +.vscode/* diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index bd6d3df..26bce64 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -325,13 +325,16 @@ Resources: - "/aws/events/${Stage}/${NS}-catchall" - Stage: !Ref Stage NS: !FindInMap [Constants, ServiceNamespace, Value] - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + RetentionInDays: !FindInMap + - LogsRetentionPeriodMap + - !Ref Stage + - Days # Permissions to allow EventBridge to send logs to CloudWatch EventBridgeCloudWatchLogGroupPolicy: Type: AWS::Logs::ResourcePolicy Properties: - PolicyName: !Sub UnicornContracts-EventBridgeToCWLogsPolicy-${Stage} + PolicyName: !Sub EvBToCWLogs-${AWS::StackName} # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument PolicyDocument: !Sub | { diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index ebf4e13..b166fba 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -3,15 +3,13 @@ AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: > - Unicorn Properties Service - events synchronization with Contract Service + - Property Approval Workflow + Unicorn Properties Service resources #### METADATA Metadata: cfn-lint: config: ignore_checks: - - ES4000 - ES6000 - I3011 - I3013 @@ -43,7 +41,9 @@ Mappings: #### CONDITIONS Conditions: - IsProd: !Equals [!Ref Stage, Prod] + IsProd: !Equals + - !Ref Stage + - Prod #### GLOBALS Globals: @@ -67,20 +67,21 @@ Globals: LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage - project: !FindInMap [Constants, ProjectName, Value] + project: !FindInMap [ Constants, ProjectName, Value ] namespace: !FindInMap [Constants, ServiceNamespace, Value] #### RESOURCES Resources: + #### SSM PARAMETERS # Services own and share their namespace - UnicornPropertiesNamespaceParam: + UnicornPropertiesNamespaceParam: Type: AWS::SSM::Parameter Properties: Type: String Name: !Sub /UniProp/${Stage}/UnicornPropertiesNamespace Value: !FindInMap [Constants, ServiceNamespace, Value] - + # Services share their event bus name and arn UnicornPropertiesEventBusNameParam: Type: AWS::SSM::Parameter @@ -88,7 +89,7 @@ Resources: Type: String Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBus Value: !GetAtt UnicornPropertiesEventBus.Name - + UnicornPropertiesEventBusArnParam: Type: AWS::SSM::Parameter Properties: @@ -96,6 +97,7 @@ Resources: Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBusArn Value: !GetAtt UnicornPropertiesEventBus.Arn + #### LAMBDA FUNCTIONS ContractStatusChangedHandlerFunction: Type: AWS::Serverless::Function @@ -200,7 +202,7 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: !FindInMap [Constants, ProjectName, Value] + Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage @@ -215,7 +217,7 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: !FindInMap [Constants, ProjectName, Value] + Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage @@ -353,13 +355,16 @@ Resources: BillingMode: PAY_PER_REQUEST Tags: - Key: project - Value: !FindInMap [Constants, ProjectName, Value] + Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace Value: !FindInMap [Constants, ServiceNamespace, Value] - Key: stage Value: !Ref Stage - #### EVENT BUS + +#### EVENT BUS + + # Event bus for Contract Service used to publish and consume events UnicornPropertiesEventBus: Type: AWS::Events::EventBus Properties: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 6425fea..8c073ee 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -67,8 +67,7 @@ Globals: POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: - !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage @@ -122,7 +121,7 @@ Resources: - !Ref Stage - Days - RequestApprovalFunction: #SQS + RequestApprovalFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/approvals_service/ @@ -155,7 +154,7 @@ Resources: - !Ref Stage - Days - PublicationApprovedFunction: #EB + PublicationApprovedFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/approvals_service/ @@ -376,8 +375,13 @@ Resources: # Catchall rule used for development purposes. UnicornWebCatchAllRule: Type: AWS::Events::Rule + Metadata: + cfn-lint: + config: + ignore_checks: + - ES4000 Properties: - Name: contracts.catchall + Name: web.catchall Description: Catch all events published by the web service. EventBusName: !Ref UnicornWebEventBus EventPattern: @@ -406,7 +410,7 @@ Resources: EventBridgeCloudWatchLogGroupPolicy: Type: AWS::Logs::ResourcePolicy Properties: - PolicyName: !Sub UnicornWeb-EventBridgeToCWLogsPolicy-${Stage} + PolicyName: !Sub EvBToCWLogs-${AWS::StackName} # Note: PolicyDocument has to be established this way. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument PolicyDocument: !Sub | { From d80db89629c8cea2f15f1445a19250c14a4c8e5e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:06:23 +1000 Subject: [PATCH 42/87] fix: renamed unicorn_web namespace in unicorn_properties schema library --- .../publicationapprovalrequested/AWSEvent.py | 0 .../publicationapprovalrequested/PublicationApprovalRequested.py | 0 .../publicationapprovalrequested/__init__.py | 0 .../publicationapprovalrequested/marshaller.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/AWSEvent.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/PublicationApprovalRequested.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/__init__.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/marshaller.py (100%) diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/AWSEvent.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/AWSEvent.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/AWSEvent.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/AWSEvent.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/PublicationApprovalRequested.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/PublicationApprovalRequested.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/PublicationApprovalRequested.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/PublicationApprovalRequested.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/__init__.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/__init__.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/__init__.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/__init__.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/marshaller.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/marshaller.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/marshaller.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/marshaller.py From 66ce914cdd69f610be7c849de9ee67dd7a585261 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:10:29 +1000 Subject: [PATCH 43/87] refactor: extracted property_id parsing into its own method --- .../publication_approved_event_handler.py | 24 ++++++++++++++++++ .../request_approval_function.py | 25 +++++++++++++------ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/unicorn_web/src/approvals_service/publication_approved_event_handler.py b/unicorn_web/src/approvals_service/publication_approved_event_handler.py index 57ac7a6..e3fce26 100644 --- a/unicorn_web/src/approvals_service/publication_approved_event_handler.py +++ b/unicorn_web/src/approvals_service/publication_approved_event_handler.py @@ -1,6 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import Tuple import os +import re + import boto3 from aws_lambda_powertools.logging import Logger @@ -19,6 +22,9 @@ if (EVENT_BUS := os.environ.get('EVENT_BUS')) is None: raise InternalServerError('EVENT_BUS environment variable is undefined') +EXPRESSION = r"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+" +TARGET_STATE = 'PENDING' + # Initialise PowerTools logger: Logger = Logger() tracer: Tracer = Tracer() @@ -30,6 +36,24 @@ table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore +@tracer.capture_method +def get_keys_for_property(property_id: str) -> Tuple[str, str]: + # Validate Property ID + if not re.fullmatch(EXPRESSION, property_id): + error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" + logger.error(error_msg) + return '', '' + + # Extract components from property_id + country, city, street, number = property_id.split('/') + + # Construct DDB PK & SK keys for this property + pk_details = f"{country}#{city}".replace(' ', '-').lower() + pk = f"PROPERTY#{pk_details}" + sk = f"{street}#{str(number)}".replace(' ', '-').lower() + return pk, sk + + @tracer.capture_method def publication_approved(event_detail, errors): """Add new property to database; responds to HTTP POST with JSON payload; generates DynamoDB structure diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 2a8a5a4..d451021 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import Tuple import os import re import json @@ -38,7 +39,7 @@ @tracer.capture_method -def send_eb_event(detail_type, resources, detail): +def publish_event(detail_type, resources, detail): try: entry = {'EventBusName': EVENT_BUS, 'Source': SERVICE_NAMESPACE, @@ -65,7 +66,7 @@ def send_eb_event(detail_type, resources, detail): @tracer.capture_method -def get_property_from_ddb(pk: str, sk: str) -> dict: +def get_property(pk: str, sk: str) -> dict: response = table.get_item( Key={ 'PK': pk, 'SK': sk }, AttributesToGet=['currency', 'status', 'listprice', 'contract', @@ -102,16 +103,26 @@ def request_approval(raw_data: dict): if not re.fullmatch(EXPRESSION, property_id): error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" logger.error(error_msg) - return + return '', '' + # Extract components from property_id country, city, street, number = property_id.split('/') # Construct DDB PK & SK keys for this property pk_details = f"{country}#{city}".replace(' ', '-').lower() pk = f"PROPERTY#{pk_details}" sk = f"{street}#{str(number)}".replace(' ', '-').lower() + return pk, sk - item = get_property_from_ddb(pk=pk, sk=sk) + +@tracer.capture_method +def request_approval(raw_data: dict): + property_id = raw_data['property_id'] + + # Validate property_id, parse it and extract DynamoDB PK/SK values + pk, sk = get_keys_for_property(property_id=property_id) + # Get property details from database + item = get_property(pk=pk, sk=sk) if (status := item.pop('status')) in [ 'APPROVED', 'DECLINED', 'PENDING' ]: logger.info(f"Property '{property_id}' is already {status}; no action taken") @@ -127,11 +138,9 @@ def request_approval(raw_data: dict): item['status'] = TARGET_STATE item['listprice'] = int(item['listprice']) - send_eb_event(detail_type='PublicationApprovalRequested', - resources=[property_id], detail=item) - metrics.add_metric(name='ApprovalsRequested', unit=MetricUnit.Count, value=1) update_property_status(pk=pk, sk=sk, state=TARGET_STATE) + publish_event(detail_type='PublicationApprovalRequested', resources=[property_id], detail=item) @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore @@ -146,4 +155,4 @@ def lambda_handler(event: SQSEvent, context: LambdaContext): if http_method == 'POST': request_approval(record.json_body) else: - raise Exception(f'Unable to handle HttpMethod {http_method}') \ No newline at end of file + raise Exception(f'Unable to handle HttpMethod {http_method}') From e647a7f56d199a0807a1363b4316df06d61342a9 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:11:09 +1000 Subject: [PATCH 44/87] refactor: removed DB update stage from approval function, and removed PENDING state from flow --- unicorn_web/data/property_data.json | 6 +++--- .../request_approval_function.py | 20 +------------------ unicorn_web/template.yaml | 14 +++++-------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/unicorn_web/data/property_data.json b/unicorn_web/data/property_data.json index 445c92d..8dc0f25 100644 --- a/unicorn_web/data/property_data.json +++ b/unicorn_web/data/property_data.json @@ -16,7 +16,7 @@ "prop1_interior2.jpg", "prop1_interior3.jpg" ], - "status": "NEW" + "status": "PENDING" }, { "PK": "PROPERTY#usa#main-town", @@ -35,7 +35,7 @@ "prop2_interior1.jpg", "prop2_interior2.jpg" ], - "status": "NEW" + "status": "PENDING" }, { "PK": "PROPERTY#usa#anytown", @@ -54,6 +54,6 @@ "prop3_interior2.jpg", "prop3_interior3.jpg" ], - "status": "NEW" + "status": "PENDING" } ] \ No newline at end of file diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index d451021..34ae9e1 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -81,24 +81,7 @@ def get_property(pk: str, sk: str) -> dict: @tracer.capture_method -def update_property_status(pk: str, sk: str, state: str) -> bool: - logger.info(f"Updating status of property {pk},{sk} in DynamoDB to {state}") - response = table.update_item( - Key={ 'PK': pk, 'SK': sk }, - AttributeUpdates={ - 'status': { - 'Value': state, - 'Action': 'PUT', - } - }, - ) - return response['ResponseMetadata']['HTTPStatusCode'] == 22 - - -@tracer.capture_method -def request_approval(raw_data: dict): - property_id = raw_data['property_id'] - +def get_keys_for_property(property_id: str) -> Tuple[str, str]: # Validate Property ID if not re.fullmatch(EXPRESSION, property_id): error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" @@ -139,7 +122,6 @@ def request_approval(raw_data: dict): item['listprice'] = int(item['listprice']) metrics.add_metric(name='ApprovalsRequested', unit=MetricUnit.Count, value=1) - update_property_status(pk=pk, sk=sk, state=TARGET_STATE) publish_event(detail_type='PublicationApprovalRequested', resources=[property_id], detail=item) diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 8c073ee..beb151c 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -131,8 +131,6 @@ Resources: EventBusName: !Ref UnicornWebEventBus - DynamoDBReadPolicy: TableName: !Ref WebTable - - DynamoDBWritePolicy: - TableName: !Ref WebTable Events: IngestQueue: Type: SQS @@ -154,14 +152,12 @@ Resources: - !Ref Stage - Days - PublicationApprovedFunction: + PublicationApprovedEventHandlerFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/approvals_service/ Handler: publication_approved_event_handler.lambda_handler Policies: - - DynamoDBReadPolicy: - TableName: !Ref WebTable - DynamoDBWritePolicy: TableName: !Ref WebTable Events: @@ -181,12 +177,12 @@ Resources: DeadLetterConfig: Arn: !GetAtt UnicornWebEventBusDLQ.Arn - PublicationApprovedFunctionLogGroup: + PublicationApprovedEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - LogGroupName: !Sub "/aws/lambda/${PublicationApprovedFunction}" + LogGroupName: !Sub "/aws/lambda/${PublicationApprovedEventHandlerFunction}" RetentionInDays: !FindInMap - LogsRetentionPeriodMap - !Ref Stage @@ -474,9 +470,9 @@ Outputs: RequestApprovalFunctionArn: Description: Approval function ARN Value: !GetAtt RequestApprovalFunction.Arn - PublicationApprovedFunctionArn: + PublicationApprovedEventHandleFunctionArn: Description: Publication evaluation event handler function ARN - Value: !GetAtt PublicationApprovedFunction.Arn + Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn IsProd: Description: Is Production? From 03bcc8962e4353a859d1fa6e90155f9c41414586 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:21:26 +1000 Subject: [PATCH 45/87] feat: Split event schemas and registry to each service --- .../integration/ContractStatusChanged.json | 85 +++++++ .../integration/event-schemas.yaml | 140 +++++++++++ unicorn_contracts/samconfig.yaml | 15 ++ .../integration/event-schemas.yaml | 46 ++++ unicorn_properties/samconfig.yaml | 13 + .../PublicationApprovalRequested.json | 126 ++++++++++ .../PublicationEvaluationCompleted.json | 76 ++++++ .../integration}/event-schemas.yaml | 232 +++++++++++------- unicorn_web/samconfig.yaml | 15 ++ 9 files changed, 664 insertions(+), 84 deletions(-) create mode 100644 unicorn_contracts/integration/ContractStatusChanged.json create mode 100644 unicorn_contracts/integration/event-schemas.yaml create mode 100644 unicorn_properties/integration/event-schemas.yaml create mode 100644 unicorn_web/integration/PublicationApprovalRequested.json create mode 100644 unicorn_web/integration/PublicationEvaluationCompleted.json rename {unicorn_properties/cloudformation => unicorn_web/integration}/event-schemas.yaml (58%) diff --git a/unicorn_contracts/integration/ContractStatusChanged.json b/unicorn_contracts/integration/ContractStatusChanged.json new file mode 100644 index 0000000..9d09b60 --- /dev/null +++ b/unicorn_contracts/integration/ContractStatusChanged.json @@ -0,0 +1,85 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "ContractStatusChanged" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "ContractStatusChanged", + "x-amazon-events-source": "unicorn.contracts", + "properties": { + "detail": { + "$ref": "#/components/schemas/ContractStatusChanged" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "ContractStatusChanged": { + "type": "object", + "required": [ + "contract_last_modified_on", + "contract_id", + "contract_status", + "property_id" + ], + "properties": { + "contract_id": { + "type": "string" + }, + "contract_last_modified_on": { + "type": "string", + "format": "date-time" + }, + "contract_status": { + "type": "string" + }, + "property_id": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml new file mode 100644 index 0000000..3a52b86 --- /dev/null +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -0,0 +1,140 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Event Schemas for use by the Contracts Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES +Resources: + EventRegistry: + Type: AWS::EventSchemas::Registry + Properties: + Description: 'Event schemas for Unicorn Contracts' + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + ContractApprovedEventSchema: + Type: AWS::EventSchemas::Schema + Properties: + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@ContractStatusChanged' + Description: 'The schema for a request to publish a property' + Content: > + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "ContractStatusChanged" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "ContractStatusChanged", + "x-amazon-events-source": "unicorn.contracts", + "properties": { + "detail": { + "$ref": "#/components/schemas/ContractStatusChanged" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "ContractStatusChanged": { + "type": "object", + "required": [ + "contract_last_modified_on", + "contract_id", + "contract_status", + "property_id" + ], + "properties": { + "contract_id": { + "type": "string" + }, + "contract_last_modified_on": { + "type": "string", + "format": "date-time" + }, + "contract_status": { + "type": "string" + }, + "property_id": { + "type": "string" + } + } + } + } + } + } diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 015b728..d25be65 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -32,6 +32,21 @@ default: parameters: warm_containers: EAGER +schema: + global: + parameters: + stack_name: uni-prop-local-contract-schema + s3_prefix: uni-prop-local-contract-schema + resolve_s3: true + deploy: + parameters: + template_file: integration/event-schemas.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml new file mode 100644 index 0000000..ec876bd --- /dev/null +++ b/unicorn_properties/integration/event-schemas.yaml @@ -0,0 +1,46 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Event Schemas for use by the Properties Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES +Resources: + EventRegistry: + Type: AWS::EventSchemas::Registry + Properties: + Description: 'Event schemas for Unicorn Properties' + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 386c607..4fc5c54 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -32,6 +32,19 @@ default: parameters: warm_containers: EAGER +schema: + deploy: + parameters: + stack_name: uni-prop-local-properties-schema + s3_prefix: uni-prop-local-properties-schema + template_file: integration/event-schemas.yaml + resolve_s3: true + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: diff --git a/unicorn_web/integration/PublicationApprovalRequested.json b/unicorn_web/integration/PublicationApprovalRequested.json new file mode 100644 index 0000000..a879a32 --- /dev/null +++ b/unicorn_web/integration/PublicationApprovalRequested.json @@ -0,0 +1,126 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationApprovalRequested" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationApprovalRequested", + "x-amazon-events-source": "unicorn.web", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationApprovalRequested" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationApprovalRequested": { + "type": "object", + "required": [ + "images", + "address", + "listprice", + "contract", + "description", + "currency", + "property_id", + "status" + ], + "properties": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "contract": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "listprice": { + "type": "string" + }, + "property_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "Address": { + "type": "object", + "required": [ + "country", + "number", + "city", + "street" + ], + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "number": { + "type": "string" + }, + "street": { + "type": "string" + } + } + } + } + } +} diff --git a/unicorn_web/integration/PublicationEvaluationCompleted.json b/unicorn_web/integration/PublicationEvaluationCompleted.json new file mode 100644 index 0000000..168ca94 --- /dev/null +++ b/unicorn_web/integration/PublicationEvaluationCompleted.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationEvaluationCompleted" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "unicorn.web", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationEvaluationCompleted" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationEvaluationCompleted": { + "type": "object", + "required": [ + "property_id", + "evaluation_result" + ], + "properties": { + "property_id": { + "type": "string" + }, + "evaluation_result": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/unicorn_properties/cloudformation/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml similarity index 58% rename from unicorn_properties/cloudformation/event-schemas.yaml rename to unicorn_web/integration/event-schemas.yaml index 3c56f43..b99fabe 100644 --- a/unicorn_properties/cloudformation/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -1,40 +1,85 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Metadata: - License: Apache-2.0 -Description: 'Event Schemas for use by the Property Service' +Description: 'Event Schemas for use by the Web Service' + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + +#### RESOURCES Resources: - UnicornPropertiesEventRegistry: - Type: AWS::EventSchemas::Registry - Properties: - Description: 'Event schemas for Unicorn Properties' - RegistryName: 'unicorn.contracts' - UnicornWebEventRegistry: + EventRegistry: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Web' - RegistryName: 'unicorn.web' - ContractApprovedEventSchema: + RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" + + EventRegistryPolicy: + Type: AWS::EventSchemas::RegistryPolicy + Properties: + RegistryName: !GetAtt EventRegistry.RegistryName + Policy: + Version: '2012-10-17' + Statement: + - Sid: AllowExternalServices + Effect: Allow + Principal: + AWS: + - !Ref AWS::AccountId + Action: + - schemas:DescribeCodeBinding + - schemas:DescribeRegistry + - schemas:DescribeSchema + - schemas:GetCodeBindingSource + - schemas:ListSchemas + - schemas:ListSchemaVersions + - schemas:SearchSchemas + Resource: + - !GetAtt EventRegistry.RegistryArn + - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + PublicationApprovalRequested: Type: AWS::EventSchemas::Schema Properties: - SchemaName: 'unicorn.contracts@ContractStatusChanged' - Content: ' + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationApprovalRequested' + Description: 'The schema for a request to publish a property' + Content: > { "openapi": "3.0.0", "info": { "version": "1.0.0", - "title": "ContractStatusChanged" + "title": "PublicationApprovalRequested" }, "paths": {}, "components": { "schemas": { "AWSEvent": { "type": "object", - "required": ["detail-type", "resources", "detail", "id", "source", "time", "region", "version", "account"], - "x-amazon-events-detail-type": "ContractStatusChanged", - "x-amazon-events-source": "unicorn.contracts", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationApprovalRequested", + "x-amazon-events-source": "unicorn.web", "properties": { "detail": { - "$ref": "#/components/schemas/ContractStatusChanged" + "$ref": "#/components/schemas/PublicationApprovalRequested" }, "account": { "type": "string" @@ -51,7 +96,7 @@ Resources: "resources": { "type": "array", "items": { - "type": "object" + "type": "string" } }, "source": { @@ -66,53 +111,111 @@ Resources: } } }, - "ContractStatusChanged": { + "PublicationApprovalRequested": { "type": "object", - "required": ["contract_last_modified_on", "contract_id", "contract_status", "property_id"], + "required": [ + "images", + "address", + "listprice", + "contract", + "description", + "currency", + "property_id", + "status" + ], "properties": { - "contract_id": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "contract": { "type": "string" }, - "contract_last_modified_on": { - "type": "string", - "format": "date-time" + "currency": { + "type": "string" }, - "contract_status": { + "description": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "listprice": { "type": "string" }, "property_id": { "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "Address": { + "type": "object", + "required": [ + "country", + "number", + "city", + "street" + ], + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "number": { + "type": "string" + }, + "street": { + "type": "string" } } } } } - }' - Description: 'The schema for a property approval event' - RegistryName: !GetAtt UnicornPropertiesEventRegistry.RegistryName - Type: 'OpenApi3' - PublicationApprovalRequested: + } + + + PublicationEvaluationCompleted: Type: AWS::EventSchemas::Schema Properties: - SchemaName: 'unicorn.web@PublicationApprovalRequested' - Content: ' + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' + Description: 'The schema for when a property evaluation is completed' + Content: > { "openapi": "3.0.0", "info": { "version": "1.0.0", - "title": "PublicationApprovalRequested" + "title": "PublicationEvaluationCompleted" }, "paths": {}, "components": { "schemas": { "AWSEvent": { "type": "object", - "required": ["detail-type", "resources", "detail", "id", "source", "time", "region", "version", "account"], - "x-amazon-events-detail-type": "PublicationApprovalRequested", - "x-amazon-events-source": "unicorn.properties.web", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "unicorn.web", "properties": { "detail": { - "$ref": "#/components/schemas/PublicationApprovalRequested" + "$ref": "#/components/schemas/PublicationEvaluationCompleted" }, "account": { "type": "string" @@ -144,60 +247,21 @@ Resources: } } }, - "PublicationApprovalRequested": { + "PublicationEvaluationCompleted": { "type": "object", - "required": ["images", "address", "listprice", "contract", "description", "currency", "property_id", "status"], + "required": [ + "property_id", + "evaluation_result" + ], "properties": { - "address": { - "$ref": "#/components/schemas/Address" - }, - "contract": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "description": { - "type": "string" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "listprice": { - "type": "string" - }, "property_id": { "type": "string" }, - "status": { - "type": "string" - } - } - }, - "Address": { - "type": "object", - "required": ["country", "number", "city", "street"], - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "number": { - "type": "string" - }, - "street": { + "evaluation_result": { "type": "string" } } } } } - }' - Description: 'The schema for a request to publish a property' - RegistryName: !GetAtt UnicornWebEventRegistry.RegistryName - Type: 'OpenApi3' + } \ No newline at end of file diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index 138b1d9..6eb745f 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -32,6 +32,21 @@ default: parameters: warm_containers: EAGER +schema: + global: + parameters: + stack_name: uni-prop-local-web-schema + s3_prefix: uni-prop-local-web-schema + resolve_s3: true + deploy: + parameters: + template_file: integration/event-schemas.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + parameter_overrides: + - "Stage=Local" + subscribers: global: parameters: From be057fea4e92fde920b295107a674ef31cd4050a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:26:32 +1000 Subject: [PATCH 46/87] chore: Add license header to state_machine definition --- unicorn_properties/state_machine/property_approval.asl.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unicorn_properties/state_machine/property_approval.asl.yaml b/unicorn_properties/state_machine/property_approval.asl.yaml index 4adac6b..4cdff43 100644 --- a/unicorn_properties/state_machine/property_approval.asl.yaml +++ b/unicorn_properties/state_machine/property_approval.asl.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 Comment: >- The property approval workflow ensures that its images and content is safe to publish and that there is an approved contract in place before the listing is From 11247c314eb3405cd1b68897ae9fa166ccc305e2 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 12:33:33 +1000 Subject: [PATCH 47/87] fix: moved schema for PublicationEvaluationCompleted event from unicorn_web to unicorn_properties --- .../integration/event-schemas.yaml | 4 +- .../PublicationEvaluationCompleted.json | 0 .../integration/event-schemas.yaml | 85 ++++++++++++++++++ unicorn_web/integration/event-schemas.yaml | 90 +------------------ 4 files changed, 89 insertions(+), 90 deletions(-) rename {unicorn_web => unicorn_properties}/integration/PublicationEvaluationCompleted.json (100%) diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index 3a52b86..5208b72 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -52,7 +52,7 @@ Resources: RegistryName: !GetAtt EventRegistry.RegistryName SchemaName: !Sub '${EventRegistry.RegistryName}@ContractStatusChanged' Description: 'The schema for a request to publish a property' - Content: > + Content: !Sub | { "openapi": "3.0.0", "info": { @@ -76,7 +76,7 @@ Resources: "account" ], "x-amazon-events-detail-type": "ContractStatusChanged", - "x-amazon-events-source": "unicorn.contracts", + "x-amazon-events-source": "${EventRegistry.RegistryName}", "properties": { "detail": { "$ref": "#/components/schemas/ContractStatusChanged" diff --git a/unicorn_web/integration/PublicationEvaluationCompleted.json b/unicorn_properties/integration/PublicationEvaluationCompleted.json similarity index 100% rename from unicorn_web/integration/PublicationEvaluationCompleted.json rename to unicorn_properties/integration/PublicationEvaluationCompleted.json diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml index ec876bd..05088c2 100644 --- a/unicorn_properties/integration/event-schemas.yaml +++ b/unicorn_properties/integration/event-schemas.yaml @@ -44,3 +44,88 @@ Resources: Resource: - !GetAtt EventRegistry.RegistryArn - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + + PublicationEvaluationCompleted: + Type: AWS::EventSchemas::Schema + Properties: + Type: 'OpenApi3' + RegistryName: !GetAtt EventRegistry.RegistryName + SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' + Description: 'The schema for when a property evaluation is completed' + Content: !Sub | + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationEvaluationCompleted" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "${EventRegistry.RegistryName}", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationEvaluationCompleted" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "PublicationEvaluationCompleted": { + "type": "object", + "required": [ + "property_id", + "evaluation_result" + ], + "properties": { + "property_id": { + "type": "string" + }, + "evaluation_result": { + "type": "string" + } + } + } + } + } + } diff --git a/unicorn_web/integration/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml index b99fabe..6091b4d 100644 --- a/unicorn_web/integration/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -52,7 +52,7 @@ Resources: RegistryName: !GetAtt EventRegistry.RegistryName SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationApprovalRequested' Description: 'The schema for a request to publish a property' - Content: > + Content: !Sub | { "openapi": "3.0.0", "info": { @@ -76,7 +76,7 @@ Resources: "account" ], "x-amazon-events-detail-type": "PublicationApprovalRequested", - "x-amazon-events-source": "unicorn.web", + "x-amazon-events-source": "${EventRegistry.RegistryName}", "properties": { "detail": { "$ref": "#/components/schemas/PublicationApprovalRequested" @@ -179,89 +179,3 @@ Resources: } } } - - - PublicationEvaluationCompleted: - Type: AWS::EventSchemas::Schema - Properties: - Type: 'OpenApi3' - RegistryName: !GetAtt EventRegistry.RegistryName - SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' - Description: 'The schema for when a property evaluation is completed' - Content: > - { - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "PublicationEvaluationCompleted" - }, - "paths": {}, - "components": { - "schemas": { - "AWSEvent": { - "type": "object", - "required": [ - "detail-type", - "resources", - "detail", - "id", - "source", - "time", - "region", - "version", - "account" - ], - "x-amazon-events-detail-type": "PublicationEvaluationCompleted", - "x-amazon-events-source": "unicorn.web", - "properties": { - "detail": { - "$ref": "#/components/schemas/PublicationEvaluationCompleted" - }, - "account": { - "type": "string" - }, - "detail-type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "region": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "source": { - "type": "string" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" - } - } - }, - "PublicationEvaluationCompleted": { - "type": "object", - "required": [ - "property_id", - "evaluation_result" - ], - "properties": { - "property_id": { - "type": "string" - }, - "evaluation_result": { - "type": "string" - } - } - } - } - } - } \ No newline at end of file From 2c909e84ef666270204e9ca80ac36654dd6ce4ca Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:06:23 +1000 Subject: [PATCH 48/87] fix: renamed unicorn_web namespace in unicorn_properties schema library --- .../publicationapprovalrequested/AWSEvent.py | 0 .../publicationapprovalrequested/PublicationApprovalRequested.py | 0 .../publicationapprovalrequested/__init__.py | 0 .../publicationapprovalrequested/marshaller.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/AWSEvent.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/PublicationApprovalRequested.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/__init__.py (100%) rename unicorn_properties/src/schema/{unicorn_properties_web => unicorn_web}/publicationapprovalrequested/marshaller.py (100%) diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/AWSEvent.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/AWSEvent.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/AWSEvent.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/AWSEvent.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/PublicationApprovalRequested.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/PublicationApprovalRequested.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/PublicationApprovalRequested.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/PublicationApprovalRequested.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/__init__.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/__init__.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/__init__.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/__init__.py diff --git a/unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/marshaller.py b/unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/marshaller.py similarity index 100% rename from unicorn_properties/src/schema/unicorn_properties_web/publicationapprovalrequested/marshaller.py rename to unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/marshaller.py From da21f8f8a16c97fa9b69416a6bc630a79d83176d Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:10:29 +1000 Subject: [PATCH 49/87] refactor: extracted property_id parsing into its own method --- .../publication_approved_event_handler.py | 24 ++++++++++++++++++ .../request_approval_function.py | 25 +++++++++++++------ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/unicorn_web/src/approvals_service/publication_approved_event_handler.py b/unicorn_web/src/approvals_service/publication_approved_event_handler.py index 57ac7a6..e3fce26 100644 --- a/unicorn_web/src/approvals_service/publication_approved_event_handler.py +++ b/unicorn_web/src/approvals_service/publication_approved_event_handler.py @@ -1,6 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import Tuple import os +import re + import boto3 from aws_lambda_powertools.logging import Logger @@ -19,6 +22,9 @@ if (EVENT_BUS := os.environ.get('EVENT_BUS')) is None: raise InternalServerError('EVENT_BUS environment variable is undefined') +EXPRESSION = r"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+" +TARGET_STATE = 'PENDING' + # Initialise PowerTools logger: Logger = Logger() tracer: Tracer = Tracer() @@ -30,6 +36,24 @@ table = dynamodb.Table(DYNAMODB_TABLE) # type: ignore +@tracer.capture_method +def get_keys_for_property(property_id: str) -> Tuple[str, str]: + # Validate Property ID + if not re.fullmatch(EXPRESSION, property_id): + error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" + logger.error(error_msg) + return '', '' + + # Extract components from property_id + country, city, street, number = property_id.split('/') + + # Construct DDB PK & SK keys for this property + pk_details = f"{country}#{city}".replace(' ', '-').lower() + pk = f"PROPERTY#{pk_details}" + sk = f"{street}#{str(number)}".replace(' ', '-').lower() + return pk, sk + + @tracer.capture_method def publication_approved(event_detail, errors): """Add new property to database; responds to HTTP POST with JSON payload; generates DynamoDB structure diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 2a8a5a4..d451021 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +from typing import Tuple import os import re import json @@ -38,7 +39,7 @@ @tracer.capture_method -def send_eb_event(detail_type, resources, detail): +def publish_event(detail_type, resources, detail): try: entry = {'EventBusName': EVENT_BUS, 'Source': SERVICE_NAMESPACE, @@ -65,7 +66,7 @@ def send_eb_event(detail_type, resources, detail): @tracer.capture_method -def get_property_from_ddb(pk: str, sk: str) -> dict: +def get_property(pk: str, sk: str) -> dict: response = table.get_item( Key={ 'PK': pk, 'SK': sk }, AttributesToGet=['currency', 'status', 'listprice', 'contract', @@ -102,16 +103,26 @@ def request_approval(raw_data: dict): if not re.fullmatch(EXPRESSION, property_id): error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" logger.error(error_msg) - return + return '', '' + # Extract components from property_id country, city, street, number = property_id.split('/') # Construct DDB PK & SK keys for this property pk_details = f"{country}#{city}".replace(' ', '-').lower() pk = f"PROPERTY#{pk_details}" sk = f"{street}#{str(number)}".replace(' ', '-').lower() + return pk, sk - item = get_property_from_ddb(pk=pk, sk=sk) + +@tracer.capture_method +def request_approval(raw_data: dict): + property_id = raw_data['property_id'] + + # Validate property_id, parse it and extract DynamoDB PK/SK values + pk, sk = get_keys_for_property(property_id=property_id) + # Get property details from database + item = get_property(pk=pk, sk=sk) if (status := item.pop('status')) in [ 'APPROVED', 'DECLINED', 'PENDING' ]: logger.info(f"Property '{property_id}' is already {status}; no action taken") @@ -127,11 +138,9 @@ def request_approval(raw_data: dict): item['status'] = TARGET_STATE item['listprice'] = int(item['listprice']) - send_eb_event(detail_type='PublicationApprovalRequested', - resources=[property_id], detail=item) - metrics.add_metric(name='ApprovalsRequested', unit=MetricUnit.Count, value=1) update_property_status(pk=pk, sk=sk, state=TARGET_STATE) + publish_event(detail_type='PublicationApprovalRequested', resources=[property_id], detail=item) @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore @@ -146,4 +155,4 @@ def lambda_handler(event: SQSEvent, context: LambdaContext): if http_method == 'POST': request_approval(record.json_body) else: - raise Exception(f'Unable to handle HttpMethod {http_method}') \ No newline at end of file + raise Exception(f'Unable to handle HttpMethod {http_method}') From e85cd824653c35e5451a48fcf11e52ad35b5cae6 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Tue, 5 Sep 2023 17:11:09 +1000 Subject: [PATCH 50/87] refactor: removed DB update stage from approval function, and removed PENDING state from flow --- unicorn_web/data/property_data.json | 6 +++--- .../request_approval_function.py | 20 +------------------ unicorn_web/template.yaml | 14 +++++-------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/unicorn_web/data/property_data.json b/unicorn_web/data/property_data.json index 445c92d..8dc0f25 100644 --- a/unicorn_web/data/property_data.json +++ b/unicorn_web/data/property_data.json @@ -16,7 +16,7 @@ "prop1_interior2.jpg", "prop1_interior3.jpg" ], - "status": "NEW" + "status": "PENDING" }, { "PK": "PROPERTY#usa#main-town", @@ -35,7 +35,7 @@ "prop2_interior1.jpg", "prop2_interior2.jpg" ], - "status": "NEW" + "status": "PENDING" }, { "PK": "PROPERTY#usa#anytown", @@ -54,6 +54,6 @@ "prop3_interior2.jpg", "prop3_interior3.jpg" ], - "status": "NEW" + "status": "PENDING" } ] \ No newline at end of file diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index d451021..34ae9e1 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -81,24 +81,7 @@ def get_property(pk: str, sk: str) -> dict: @tracer.capture_method -def update_property_status(pk: str, sk: str, state: str) -> bool: - logger.info(f"Updating status of property {pk},{sk} in DynamoDB to {state}") - response = table.update_item( - Key={ 'PK': pk, 'SK': sk }, - AttributeUpdates={ - 'status': { - 'Value': state, - 'Action': 'PUT', - } - }, - ) - return response['ResponseMetadata']['HTTPStatusCode'] == 22 - - -@tracer.capture_method -def request_approval(raw_data: dict): - property_id = raw_data['property_id'] - +def get_keys_for_property(property_id: str) -> Tuple[str, str]: # Validate Property ID if not re.fullmatch(EXPRESSION, property_id): error_msg = f"Invalid property id '{property_id}'; must conform to regular expression: {EXPRESSION}" @@ -139,7 +122,6 @@ def request_approval(raw_data: dict): item['listprice'] = int(item['listprice']) metrics.add_metric(name='ApprovalsRequested', unit=MetricUnit.Count, value=1) - update_property_status(pk=pk, sk=sk, state=TARGET_STATE) publish_event(detail_type='PublicationApprovalRequested', resources=[property_id], detail=item) diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 8c073ee..beb151c 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -131,8 +131,6 @@ Resources: EventBusName: !Ref UnicornWebEventBus - DynamoDBReadPolicy: TableName: !Ref WebTable - - DynamoDBWritePolicy: - TableName: !Ref WebTable Events: IngestQueue: Type: SQS @@ -154,14 +152,12 @@ Resources: - !Ref Stage - Days - PublicationApprovedFunction: + PublicationApprovedEventHandlerFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/approvals_service/ Handler: publication_approved_event_handler.lambda_handler Policies: - - DynamoDBReadPolicy: - TableName: !Ref WebTable - DynamoDBWritePolicy: TableName: !Ref WebTable Events: @@ -181,12 +177,12 @@ Resources: DeadLetterConfig: Arn: !GetAtt UnicornWebEventBusDLQ.Arn - PublicationApprovedFunctionLogGroup: + PublicationApprovedEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - LogGroupName: !Sub "/aws/lambda/${PublicationApprovedFunction}" + LogGroupName: !Sub "/aws/lambda/${PublicationApprovedEventHandlerFunction}" RetentionInDays: !FindInMap - LogsRetentionPeriodMap - !Ref Stage @@ -474,9 +470,9 @@ Outputs: RequestApprovalFunctionArn: Description: Approval function ARN Value: !GetAtt RequestApprovalFunction.Arn - PublicationApprovedFunctionArn: + PublicationApprovedEventHandleFunctionArn: Description: Publication evaluation event handler function ARN - Value: !GetAtt PublicationApprovedFunction.Arn + Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn IsProd: Description: Is Production? From 6e0dacecffdb969778c86fa43aaf2c2c52f73797 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Fri, 8 Sep 2023 00:29:07 +0800 Subject: [PATCH 51/87] feat: updating Stage values to lowercase; updating stage references and parameters; sync with dotnet --- .gitignore | 1 + .vscode/settings.json | 0 unicorn_contracts/README.md | 24 +++++++ .../integration/event-schemas.yaml | 10 +-- .../subscriber-policies.yaml | 14 ++-- unicorn_contracts/samconfig.yaml | 20 +++--- unicorn_contracts/template.yaml | 21 +++--- .../integration/event-schemas.yaml | 10 +-- .../subscriber-policies.yaml | 14 ++-- .../{ => integration}/subscriptions.yaml | 24 +++---- unicorn_properties/samconfig.yaml | 20 +++--- unicorn_properties/template.yaml | 60 ++++++++-------- unicorn_web/api.yaml | 8 ++- unicorn_web/integration/event-schemas.yaml | 10 +-- .../subscriber-policies.yaml | 14 ++-- .../{ => integration}/subscriptions.yaml | 16 ++--- unicorn_web/samconfig.yaml | 21 +++--- unicorn_web/template.yaml | 69 +++++-------------- 18 files changed, 178 insertions(+), 178 deletions(-) delete mode 100644 .vscode/settings.json rename unicorn_contracts/{ => integration}/subscriber-policies.yaml (80%) rename unicorn_properties/{ => integration}/subscriber-policies.yaml (80%) rename unicorn_properties/{ => integration}/subscriptions.yaml (81%) rename unicorn_web/{ => integration}/subscriber-policies.yaml (80%) rename unicorn_web/{ => integration}/subscriptions.yaml (81%) diff --git a/.gitignore b/.gitignore index 54d784a..8ec6b06 100755 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea/* **/.aws-sam/ .vscode/* +.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e69de29..0000000 diff --git a/unicorn_contracts/README.md b/unicorn_contracts/README.md index 8d4ce6f..fcd940e 100644 --- a/unicorn_contracts/README.md +++ b/unicorn_contracts/README.md @@ -30,3 +30,27 @@ Here is an example of an event that is published to EventBridge: } } ``` + +### Testing the APIs + +```bash +export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-contract --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` + +curl --location --request POST "${API}contract" \ +--header 'Content-Type: application/json' \ +--data-raw '{ +"address": { +"country": "USA", +"city": "Anytown", +"street": "Main Street", +"number": 111 +}, +"seller_name": "John Doe", +"property_id": "usa/anytown/main-street/111" +}' + + +curl --location --request PUT "${API}contract" \ +--header 'Content-Type: application/json' \ +--data-raw '{"property_id": "usa/anytown/main-street/111"}' | jq +``` diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index 5208b72..a9ca464 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -7,11 +7,11 @@ Description: 'Event Schemas for use by the Contracts Service' Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### RESOURCES Resources: @@ -19,7 +19,7 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Contracts' - RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy diff --git a/unicorn_contracts/subscriber-policies.yaml b/unicorn_contracts/integration/subscriber-policies.yaml similarity index 80% rename from unicorn_contracts/subscriber-policies.yaml rename to unicorn_contracts/integration/subscriber-policies.yaml index da13faa..de072e5 100644 --- a/unicorn_contracts/subscriber-policies.yaml +++ b/unicorn_contracts/integration/subscriber-policies.yaml @@ -8,18 +8,18 @@ Description: Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod Resources: # Update this policy as you get new subscribers by adding their namespace to events:source CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBus}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" Statement: Effect: Allow @@ -36,12 +36,12 @@ Resources: Resource: - !Sub - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBus}}" + - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index d25be65..513290d 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -18,7 +18,7 @@ default: fail_on_empty_changeset: false capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" validate: parameters: lint: true @@ -32,11 +32,11 @@ default: parameters: warm_containers: EAGER -schema: +schemas: global: parameters: - stack_name: uni-prop-local-contract-schema - s3_prefix: uni-prop-local-contract-schema + stack_name: uni-prop-local-contract-schemas + s3_prefix: uni-prop-local-contract-schemas resolve_s3: true deploy: parameters: @@ -45,20 +45,20 @@ schema: fail_on_empty_changeset: false on_failure: ROLLBACK parameter_overrides: - - "Stage=Local" + - "Stage=local" -subscribers: +subscriber-policies: global: parameters: - stack_name: uni-prop-local-contract-subscribers - s3_prefix: uni-prop-local-contract-subscribers + stack_name: uni-prop-local-contract-subscriber-policies + s3_prefix: uni-prop-local-contract-subscriber-policies resolve_s3: true deploy: parameters: - template_file: subscriber-policies.yaml + template_file: ./integration/subscriber-policies.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 26bce64..f68c65c 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -21,20 +21,20 @@ Metadata: Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### MAPPINGS Mappings: LogsRetentionPeriodMap: - Local: + local: Days: 3 - Dev: + dev: Days: 3 - Prod: + prod: Days: 14 Constants: ServiceNamespace: @@ -79,13 +79,14 @@ Globals: #### RESOURCES Resources: + #### SSM PARAMETERS # Services own and share their namespace UnicornContractsNamespaceParam: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsNamespace + Name: !Sub /uni-prop/${Stage}/UnicornContractsNamespace Value: !FindInMap [Constants, ServiceNamespace, Value] # Services share their event bus name and arn @@ -93,14 +94,14 @@ Resources: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsEventBus + Name: !Sub /uni-prop/${Stage}/UnicornContractsEventBus Value: !GetAtt UnicornContractsEventBus.Name UnicornContractsEventBusArnParam: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornContractsEventBusArn + Name: !Sub /uni-prop/${Stage}/UnicornContractsEventBusArn Value: !GetAtt UnicornContractsEventBus.Arn #### LAMBDA FUNCTIONS diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml index 05088c2..28689f6 100644 --- a/unicorn_properties/integration/event-schemas.yaml +++ b/unicorn_properties/integration/event-schemas.yaml @@ -7,11 +7,11 @@ Description: 'Event Schemas for use by the Properties Service' Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### RESOURCES Resources: @@ -19,7 +19,7 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Properties' - RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesNamespace}}" + RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy diff --git a/unicorn_properties/subscriber-policies.yaml b/unicorn_properties/integration/subscriber-policies.yaml similarity index 80% rename from unicorn_properties/subscriber-policies.yaml rename to unicorn_properties/integration/subscriber-policies.yaml index 0ed61c3..f9dfb95 100644 --- a/unicorn_properties/subscriber-policies.yaml +++ b/unicorn_properties/integration/subscriber-policies.yaml @@ -7,18 +7,18 @@ Description: Defines the event bus policies that determine who can create rules Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod Resources: # Update this policy as you get new subscribers by adding their namespace to events:source CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBus}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" StatementId: !Sub CreateRulePolicy-${Stage} Statement: Effect: Allow @@ -35,12 +35,12 @@ Resources: Resource: - !Sub - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBus}}" + - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_properties/subscriptions.yaml b/unicorn_properties/integration/subscriptions.yaml similarity index 81% rename from unicorn_properties/subscriptions.yaml rename to unicorn_properties/integration/subscriptions.yaml index 4d70917..7d5d406 100644 --- a/unicorn_properties/subscriptions.yaml +++ b/unicorn_properties/integration/subscriptions.yaml @@ -6,11 +6,11 @@ Description: Defines the subscriptions for Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod Resources: ContractStatusChangedSubscriptionRule: @@ -18,16 +18,16 @@ Resources: Properties: Name: properties.statuschanged-contract.pubevalcompleted Description: Constract Status Changed subscription - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsEventBusArn}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" detail-type: - ContractStatusChanged State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" RoleArn: !GetAtt UnicornContractsEventBusToUnicornPropertiesEventBusRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events @@ -50,23 +50,23 @@ Resources: Statement: - Effect: Allow Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" PublicationApprovalRequestedSubscriptionRule: Type: AWS::Events::Rule Properties: Name: properties.pubapprovalwf-web.pubapprovalrequested Description: Publication evaluation completed subscription - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" detail-type: - PublicationApprovalRequested State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" RoleArn: !GetAtt UnicornWebEventBusToUnicornPropertiesEventBusRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events @@ -89,7 +89,7 @@ Resources: Statement: - Effect: Allow Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" Outputs: ContractStatusChangedSubscription: diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 4fc5c54..039eaba 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -18,7 +18,7 @@ default: fail_on_empty_changeset: false capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" validate: parameters: lint: true @@ -32,7 +32,7 @@ default: parameters: warm_containers: EAGER -schema: +schemas: deploy: parameters: stack_name: uni-prop-local-properties-schema @@ -43,23 +43,23 @@ schema: fail_on_empty_changeset: false on_failure: ROLLBACK parameter_overrides: - - "Stage=Local" + - "Stage=local" -subscribers: +subscriber-policies: global: parameters: - stack_name: uni-prop-local-properties-subscribers - s3_prefix: uni-prop-local-properties-subscribers + stack_name: uni-prop-local-properties-subscriber-policies + s3_prefix: uni-prop-local-properties-subscriber-policies resolve_s3: true deploy: parameters: - template_file: subscriber-policies.yaml + template_file: ./integration/subscriber-policies.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" subscriptions: global: @@ -69,10 +69,10 @@ subscriptions: resolve_s3: true deploy: parameters: - template_file: subscriptions.yaml + template_file: ./integration/subscriptions.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index b166fba..6e225eb 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -18,20 +18,20 @@ Metadata: Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### MAPPINGS Mappings: LogsRetentionPeriodMap: - Local: + local: Days: 3 - Dev: + dev: Days: 3 - Prod: + prod: Days: 14 Constants: ServiceNamespace: @@ -79,7 +79,7 @@ Resources: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornPropertiesNamespace + Name: !Sub /uni-prop/${Stage}/UnicornPropertiesNamespace Value: !FindInMap [Constants, ServiceNamespace, Value] # Services share their event bus name and arn @@ -87,14 +87,14 @@ Resources: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBus + Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBus Value: !GetAtt UnicornPropertiesEventBus.Name UnicornPropertiesEventBusArnParam: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornPropertiesEventBusArn + Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBusArn Value: !GetAtt UnicornPropertiesEventBus.Arn @@ -117,7 +117,7 @@ Resources: EventBusName: !GetAtt UnicornPropertiesEventBus.Name Pattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" detail-type: - ContractStatusChanged RetryPolicy: @@ -240,7 +240,7 @@ Resources: - LambdaInvokePolicy: FunctionName: !Ref ContractExistsCheckerFunction - S3ReadPolicy: - BucketName: !Sub "{{resolve:ssm:/UniProp/${Stage}/ImagesBucket}}" + BucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}" - ComprehendBasicAccessPolicy: {} - RekognitionDetectOnlyPolicy: {} - EventBridgePutEventsPolicy: @@ -264,28 +264,28 @@ Resources: LogGroupArn: !GetAtt ApprovalStateMachineLogGroup.Arn Level: ALL IncludeExecutionData: true - Events: - PubApproReqEvent: - Type: EventBridgeRule - Properties: - RuleName: properties.pubapprovalwf-web.pubapprovalrequested - EventBusName: !GetAtt UnicornPropertiesEventBus.Name - Pattern: - source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" - detail-type: - - PublicationApprovalRequested - RetryPolicy: - MaximumRetryAttempts: 5 - MaximumEventAgeInSeconds: 900 - DeadLetterConfig: - Type: SQS - Destination: !GetAtt PropertiesServiceDLQ.Arn + # Events: + # PubApproReqEvent: + # Type: EventBridgeRule + # Properties: + # RuleName: properties.pubapprovalwf-web.pubapprovalrequested + # EventBusName: !GetAtt UnicornPropertiesEventBus.Name + # Pattern: + # source: + # - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + # detail-type: + # - PublicationApprovalRequested + # RetryPolicy: + # MaximumRetryAttempts: 5 + # MaximumEventAgeInSeconds: 900 + # DeadLetterConfig: + # Type: SQS + # Destination: !GetAtt PropertiesServiceDLQ.Arn DefinitionSubstitutions: ContractExistsChecker: !GetAtt ContractExistsCheckerFunction.Arn WaitForContractApproval: !GetAtt WaitForContractApprovalFunction.Arn ContentIntegrityValidator: !GetAtt ContentIntegrityValidatorFunction.Arn - ImageUploadBucketName: !Sub "{{resolve:ssm:/UniProp/${Stage}/ImagesBucket}}" + ImageUploadBucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}" EventBusName: !GetAtt UnicornPropertiesEventBus.Name ServiceName: !FindInMap [Constants, ServiceNamespace, Value] diff --git a/unicorn_web/api.yaml b/unicorn_web/api.yaml index 5203ba6..384cbaa 100644 --- a/unicorn_web/api.yaml +++ b/unicorn_web/api.yaml @@ -14,7 +14,11 @@ paths: required: true responses: "200": - $ref: '#/components/responses/Empty' + description: "200 response" + content: + application/json: + schema: + $ref: '#/components/responses/Empty' x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: @@ -30,7 +34,7 @@ paths: requestParameters: integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" requestTemplates: - application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" + application/json: "Action=SendMessage&MessageBody=$input.body" passthroughBehavior: never type: aws /search/{country}/{city}: diff --git a/unicorn_web/integration/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml index 6091b4d..88f9495 100644 --- a/unicorn_web/integration/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -7,11 +7,11 @@ Description: 'Event Schemas for use by the Web Service' Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### RESOURCES Resources: @@ -19,7 +19,7 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Web' - RegistryName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebNamespace}}" + RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy diff --git a/unicorn_web/subscriber-policies.yaml b/unicorn_web/integration/subscriber-policies.yaml similarity index 80% rename from unicorn_web/subscriber-policies.yaml rename to unicorn_web/integration/subscriber-policies.yaml index a60a361..79b4fd1 100644 --- a/unicorn_web/subscriber-policies.yaml +++ b/unicorn_web/integration/subscriber-policies.yaml @@ -7,18 +7,18 @@ Description: Defines the event bus policies that determine who can create rules Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod Resources: # Update this policy as you get new subscribers by adding their namespace to events:source CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBus}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" Statement: Effect: Allow @@ -35,12 +35,12 @@ Resources: Resource: - !Sub - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBus}}" + - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_web/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml similarity index 81% rename from unicorn_web/subscriptions.yaml rename to unicorn_web/integration/subscriptions.yaml index 987edb7..c2ad516 100644 --- a/unicorn_web/subscriptions.yaml +++ b/unicorn_web/integration/subscriptions.yaml @@ -6,11 +6,11 @@ Description: Defines the subscriptions for Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod Resources: PublicationEvaluationCompletedSubscriptionRule: @@ -18,16 +18,16 @@ Resources: Properties: Name: web.pubapprovalwf-properties.pubapprovalrequested Description: Constract Status Changed subscription - EventBusName: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornPropertiesEventBusArn}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" detail-type: - PublicationEvaluationCompleted State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" RoleArn: !GetAtt UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events @@ -50,7 +50,7 @@ Resources: Statement: - Effect: Allow Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/UniProp/${Stage}/UnicornWebEventBusArn}}" + Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" Outputs: PublicationEvaluationCompletedSubscription: diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index 6eb745f..ed4fd52 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -18,7 +18,7 @@ default: fail_on_empty_changeset: false capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" validate: parameters: lint: true @@ -32,7 +32,7 @@ default: parameters: warm_containers: EAGER -schema: +schemas: global: parameters: stack_name: uni-prop-local-web-schema @@ -45,23 +45,23 @@ schema: fail_on_empty_changeset: false on_failure: ROLLBACK parameter_overrides: - - "Stage=Local" + - "Stage=local" -subscribers: +subscriber-policies: global: parameters: - stack_name: uni-prop-local-web-subscribers - s3_prefix: uni-prop-local-web-subscribers + stack_name: uni-prop-local-web-subscriber-policies + s3_prefix: uni-prop-local-web-subscriber-policies resolve_s3: true deploy: parameters: - template_file: subscriber-policies.yaml + template_file: ./integration/subscriber-policies.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" subscriptions: global: @@ -71,10 +71,11 @@ subscriptions: resolve_s3: true deploy: parameters: - template_file: subscriptions.yaml + template_file: ./integration/subscriptions.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" + - "Stage=local" + \ No newline at end of file diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index beb151c..49d729c 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 -Transform: +Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: > @@ -22,20 +22,20 @@ Metadata: Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod #### MAPPINGS Mappings: LogsRetentionPeriodMap: - Local: + local: Days: 3 - Dev: + dev: Days: 3 - Prod: + prod: Days: 14 Constants: ServiceNamespace: @@ -82,7 +82,7 @@ Resources: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornWebNamespace + Name: !Sub /uni-prop/${Stage}/UnicornWebNamespace Value: !FindInMap [Constants, ServiceNamespace, Value] # Services share their event bus name and arn @@ -90,18 +90,18 @@ Resources: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornWebEventBus + Name: !Sub /uni-prop/${Stage}/UnicornWebEventBus Value: !GetAtt UnicornWebEventBus.Name UnicornWebEventBusArnParam: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/UnicornWebEventBusArn + Name: !Sub /uni-prop/${Stage}/UnicornWebEventBusArn Value: !GetAtt UnicornWebEventBus.Arn ##### LAMBDA FUNCTIONS - SearchFunction: #API + SearchFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/search_service/ @@ -116,10 +116,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${SearchFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] RequestApprovalFunction: Type: AWS::Serverless::Function @@ -147,10 +144,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${RequestApprovalFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] PublicationApprovedEventHandlerFunction: Type: AWS::Serverless::Function @@ -171,11 +165,6 @@ Resources: - !Ref UnicornWebEventBus detail-type: - PublicationEvaluationCompleted - RetryPolicy: - MaximumRetryAttempts: 5 - MaximumEventAgeInSeconds: 900 - DeadLetterConfig: - Arn: !GetAtt UnicornWebEventBusDLQ.Arn PublicationApprovedEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup @@ -183,10 +172,7 @@ Resources: DeletionPolicy: Delete Properties: LogGroupName: !Sub "/aws/lambda/${PublicationApprovedEventHandlerFunction}" - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### API GATEWAY REST API UnicornWebApi: @@ -278,10 +264,7 @@ Resources: UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: - RetentionInDays: !FindInMap - - LogsRetentionPeriodMap - - !Ref Stage - - Days + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### INGEST QUEUES UnicornWebIngestQueue: @@ -291,6 +274,7 @@ Resources: Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub UnicornWebIngestQueue-${Stage} RedrivePolicy: deadLetterTargetArn: !GetAtt UnicornWebIngestDLQ.Arn maxReceiveCount: 1 @@ -310,6 +294,7 @@ Resources: Properties: SqsManagedSseEnabled: true MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + QueueName: !Sub UnicornWebIngestDLQ-${Stage} Tags: - Key: stage Value: !Ref Stage @@ -431,22 +416,6 @@ Resources: ] } - ##### DLQs - UnicornWebEventBusDLQ: - Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - Tags: - - Key: project - Value: !FindInMap [Constants, ProjectName, Value] - - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - - Key: stage - Value: !Ref Stage - #### OUTPUTS Outputs: BaseUrl: @@ -470,7 +439,7 @@ Outputs: RequestApprovalFunctionArn: Description: Approval function ARN Value: !GetAtt RequestApprovalFunction.Arn - PublicationApprovedEventHandleFunctionArn: + PublicationApprovedEventHandlerFunctionArn: Description: Publication evaluation event handler function ARN Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn From 612525f9734bceb88539d79c1ee04300d69c7cf8 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 16:34:53 +1000 Subject: [PATCH 52/87] chore: Fix naming structures, move ServiceNamespace SSM param to SharedInfra --- .../integration/event-schemas.yaml | 6 +-- .../integration/subscriber-policies.yaml | 4 +- unicorn_contracts/samconfig.yaml | 12 +++--- unicorn_contracts/template.yaml | 38 ++++++------------- .../integration/event-schemas.yaml | 4 +- .../integration/subscriber-policies.yaml | 9 +++-- .../integration/subscriptions.yaml | 4 +- unicorn_properties/samconfig.yaml | 4 +- unicorn_properties/template.yaml | 35 ++++++----------- unicorn_web/integration/event-schemas.yaml | 2 - .../integration/subscriber-policies.yaml | 9 +++-- unicorn_web/integration/subscriptions.yaml | 4 +- unicorn_web/samconfig.yaml | 4 +- unicorn_web/template.yaml | 37 +++++++----------- 14 files changed, 68 insertions(+), 104 deletions(-) diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index f807a3c..05b3399 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -1,9 +1,10 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: 'Event Schemas for use by the Contracts Service' +Description: | + Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by Unicorn Contracts Service. -#### PARAMETERS Parameters: Stage: Type: String @@ -13,7 +14,6 @@ Parameters: - dev - prod -#### RESOURCES Resources: EventRegistry: Type: AWS::EventSchemas::Registry diff --git a/unicorn_contracts/integration/subscriber-policies.yaml b/unicorn_contracts/integration/subscriber-policies.yaml index de072e5..1b0cc0a 100644 --- a/unicorn_contracts/integration/subscriber-policies.yaml +++ b/unicorn_contracts/integration/subscriber-policies.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" -Description: +Description: | Defines the event bus policies that determine who can create rules on the event bus to subscribe to events published by the Contracts Service. @@ -20,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" - StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" + StatementId: !Sub "${AWS::StackName}-CreateRule" Statement: Effect: Allow Principal: diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 513290d..8c67cf3 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -3,8 +3,8 @@ version: 0.1 default: global: parameters: - stack_name: uni-prop-local-contract - s3_prefix: uni-prop-local-contract + stack_name: uni-prop-local-contracts + s3_prefix: uni-prop-local-contracts resolve_s3: true resolve_image_repositories: true build: @@ -35,8 +35,8 @@ default: schemas: global: parameters: - stack_name: uni-prop-local-contract-schemas - s3_prefix: uni-prop-local-contract-schemas + stack_name: uni-prop-local-contracts-schemas + s3_prefix: uni-prop-local-contracts-schemas resolve_s3: true deploy: parameters: @@ -50,8 +50,8 @@ schemas: subscriber-policies: global: parameters: - stack_name: uni-prop-local-contract-subscriber-policies - s3_prefix: uni-prop-local-contract-subscriber-policies + stack_name: uni-prop-local-contracts-subscriber-policies + s3_prefix: uni-prop-local-contracts-subscriber-policies resolve_s3: true deploy: parameters: diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index f68c65c..ec9c1ed 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -37,8 +37,6 @@ Mappings: prod: Days: 14 Constants: - ServiceNamespace: - Value: "unicorn.contracts" ProjectName: Value: "AWS Serverless Developer Experience" @@ -60,35 +58,23 @@ Globals: Environment: Variables: DYNAMODB_TABLE: !Ref ContractsTable - SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" POWERTOOLS_LOGGER_CASE: PascalCase - POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !FindInMap [ - Constants, - ServiceNamespace, - Value, - ] # Metric Namespace + POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage project: !FindInMap [Constants, ProjectName, Value] - namespace: !FindInMap [Constants, ServiceNamespace, Value] + namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" #### RESOURCES Resources: #### SSM PARAMETERS - # Services own and share their namespace - UnicornContractsNamespaceParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /uni-prop/${Stage}/UnicornContractsNamespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - # Services share their event bus name and arn UnicornContractsEventBusNameParam: Type: AWS::SSM::Parameter @@ -167,7 +153,7 @@ Resources: Tags: stage: !Ref Stage project: !FindInMap [Constants, ProjectName, Value] - namespace: !FindInMap [Constants, ServiceNamespace, Value] + namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" ContractsApiGwAccountConfig: Type: AWS::ApiGateway::Account @@ -233,7 +219,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" UnicornContractsIngestDLQ: Type: AWS::SQS::Queue @@ -249,7 +235,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" #### DYNAMODB TABLE ContractsTable: @@ -272,7 +258,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" #### EVENT BUS # Event bus for Contract Service used to publish and consume events @@ -297,7 +283,7 @@ Resources: Condition: StringEquals: events:source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" # Catchall rule used for development purposes. UnicornContractsCatchAllRule: @@ -310,7 +296,7 @@ Resources: account: - !Ref AWS::AccountId source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn @@ -325,7 +311,7 @@ Resources: LogGroupName: !Sub - "/aws/events/${Stage}/${NS}-catchall" - Stage: !Ref Stage - NS: !FindInMap [Constants, ServiceNamespace, Value] + NS: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" RetentionInDays: !FindInMap - LogsRetentionPeriodMap - !Ref Stage @@ -386,7 +372,7 @@ Resources: Target: !GetAtt UnicornContractsEventBus.Arn TargetParameters: EventBridgeEventBusParameters: - Source: !FindInMap [Constants, ServiceNamespace, Value] + Source: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" DetailType: ContractStatusChanged InputTemplate: !ToJsonString property_id: "<$.dynamodb.NewImage.property_id.S>" diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml index 28689f6..c12cae8 100644 --- a/unicorn_properties/integration/event-schemas.yaml +++ b/unicorn_properties/integration/event-schemas.yaml @@ -1,9 +1,8 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: 'Event Schemas for use by the Properties Service' +Description: Event Schemas for use by the Properties Service -#### PARAMETERS Parameters: Stage: Type: String @@ -13,7 +12,6 @@ Parameters: - dev - prod -#### RESOURCES Resources: EventRegistry: Type: AWS::EventSchemas::Registry diff --git a/unicorn_properties/integration/subscriber-policies.yaml b/unicorn_properties/integration/subscriber-policies.yaml index f9dfb95..469d548 100644 --- a/unicorn_properties/integration/subscriber-policies.yaml +++ b/unicorn_properties/integration/subscriber-policies.yaml @@ -1,8 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the event bus policies that determine who can create rules on the event bus to - subscribe to events published by the Contracts Service. +Description: | + Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by Unicorn Properties Service. Parameters: Stage: @@ -19,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" - StatementId: !Sub CreateRulePolicy-${Stage} + StatementId: !Sub "${AWS::StackName}-CreateRule" Statement: Effect: Allow Principal: @@ -41,6 +42,6 @@ Resources: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_properties/integration/subscriptions.yaml b/unicorn_properties/integration/subscriptions.yaml index 7d5d406..4b2a64f 100644 --- a/unicorn_properties/integration/subscriptions.yaml +++ b/unicorn_properties/integration/subscriptions.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the subscriptions for +Description: Defines the cross-service subscriptions for Unicorn Properties Parameters: Stage: @@ -56,7 +56,7 @@ Resources: Type: AWS::Events::Rule Properties: Name: properties.pubapprovalwf-web.pubapprovalrequested - Description: Publication evaluation completed subscription + Description: Publication Approval Requested Subscription EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" EventPattern: source: diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 039eaba..94eac2b 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -35,8 +35,8 @@ default: schemas: deploy: parameters: - stack_name: uni-prop-local-properties-schema - s3_prefix: uni-prop-local-properties-schema + stack_name: uni-prop-local-properties-schemas + s3_prefix: uni-prop-local-properties-schemas template_file: integration/event-schemas.yaml resolve_s3: true confirm_changeset: false diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 6e225eb..1ce1c1f 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -11,8 +11,7 @@ Metadata: config: ignore_checks: - ES6000 - - I3011 - - I3013 + - ES4000 #### PARAMETERS Parameters: @@ -34,8 +33,6 @@ Mappings: prod: Days: 14 Constants: - ServiceNamespace: - Value: "unicorn.properties" ProjectName: Value: "AWS Serverless Developer Experience" @@ -58,30 +55,22 @@ Globals: Variables: CONTRACT_STATUS_TABLE: !Ref ContractStatusTable EVENT_BUS: !Ref UnicornPropertiesEventBus - SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] - POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage project: !FindInMap [ Constants, ProjectName, Value ] - namespace: !FindInMap [Constants, ServiceNamespace, Value] + namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" #### RESOURCES Resources: #### SSM PARAMETERS - # Services own and share their namespace - UnicornPropertiesNamespaceParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /uni-prop/${Stage}/UnicornPropertiesNamespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - # Services share their event bus name and arn UnicornPropertiesEventBusNameParam: Type: AWS::SSM::Parameter @@ -204,7 +193,7 @@ Resources: - Key: project Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage Value: !Ref Stage @@ -219,7 +208,7 @@ Resources: - Key: project Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage Value: !Ref Stage @@ -287,7 +276,7 @@ Resources: ContentIntegrityValidator: !GetAtt ContentIntegrityValidatorFunction.Arn ImageUploadBucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}" EventBusName: !GetAtt UnicornPropertiesEventBus.Name - ServiceName: !FindInMap [Constants, ServiceNamespace, Value] + ServiceName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" #### CLOUDWATCH LOG GROUPS ContractStatusChangedHandlerFunctionLogGroup: @@ -357,7 +346,7 @@ Resources: - Key: project Value: !FindInMap [ Constants, ProjectName, Value ] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage Value: !Ref Stage @@ -386,7 +375,7 @@ Resources: Condition: StringEquals: events:source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" # Catchall rule used for development purposes. UnicornPropertiesCatchAllRule: @@ -399,7 +388,7 @@ Resources: account: - !Ref AWS::AccountId source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn @@ -414,7 +403,7 @@ Resources: LogGroupName: !Sub - "/aws/events/${Stage}/${NS}-catchall" - Stage: !Ref Stage - NS: !FindInMap [Constants, ServiceNamespace, Value] + NS: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] # Permissions to allow EventBridge to send logs to CloudWatch diff --git a/unicorn_web/integration/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml index 88f9495..91a298b 100644 --- a/unicorn_web/integration/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -3,7 +3,6 @@ AWSTemplateFormatVersion: '2010-09-09' Description: 'Event Schemas for use by the Web Service' -#### PARAMETERS Parameters: Stage: Type: String @@ -13,7 +12,6 @@ Parameters: - dev - prod -#### RESOURCES Resources: EventRegistry: Type: AWS::EventSchemas::Registry diff --git a/unicorn_web/integration/subscriber-policies.yaml b/unicorn_web/integration/subscriber-policies.yaml index 79b4fd1..3a32423 100644 --- a/unicorn_web/integration/subscriber-policies.yaml +++ b/unicorn_web/integration/subscriber-policies.yaml @@ -1,8 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the event bus policies that determine who can create rules on the event bus to - subscribe to events published by the Contracts Service. +Description: | + Defines the event bus policies that determine who can create rules on the event bus to + subscribe to events published by Unicorn Web Service. Parameters: Stage: @@ -19,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" - StatementId: !Sub "${AWS::StackName}-CreateRulePolicy-${Stage}" + StatementId: !Sub "${AWS::StackName}-CreateRule" Statement: Effect: Allow Principal: @@ -41,6 +42,6 @@ Resources: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_web/integration/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml index c2ad516..16a8819 100644 --- a/unicorn_web/integration/subscriptions.yaml +++ b/unicorn_web/integration/subscriptions.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the subscriptions for +Description: Defines the cross-service subscriptions for Unicorn Web Parameters: Stage: @@ -17,7 +17,7 @@ Resources: Type: AWS::Events::Rule Properties: Name: web.pubapprovalwf-properties.pubapprovalrequested - Description: Constract Status Changed subscription + Description: Publication Evaluation Completed Subscription EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index ed4fd52..1e4629c 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -35,8 +35,8 @@ default: schemas: global: parameters: - stack_name: uni-prop-local-web-schema - s3_prefix: uni-prop-local-web-schema + stack_name: uni-prop-local-web-schemas + s3_prefix: uni-prop-local-web-schemas resolve_s3: true deploy: parameters: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 49d729c..d727f6b 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -12,11 +12,12 @@ Metadata: cfn-lint: config: ignore_checks: - - I3042 + - WS2001 + - ES6000 + - ES4000 - ES1001 - ES1007 - - ES6000 - - WS2001 + # - I3042 #### PARAMETERS Parameters: @@ -38,8 +39,6 @@ Mappings: prod: Days: 14 Constants: - ServiceNamespace: - Value: "unicorn.web" ProjectName: Value: "AWS Serverless Developer Experience" @@ -62,29 +61,21 @@ Globals: Variables: DYNAMODB_TABLE: !Ref WebTable EVENT_BUS: !Ref UnicornWebEventBus - SERVICE_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] - POWERTOOLS_SERVICE_NAME: !FindInMap [Constants, ServiceNamespace, Value] + SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: !FindInMap [Constants, ServiceNamespace, Value] + POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage project: !FindInMap [Constants, ProjectName, Value] - namespace: !FindInMap [Constants, ServiceNamespace, Value] + namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" ##### RESOURCES Resources: #### SSM PARAMETERS - # Services own and share their namespace - UnicornWebNamespaceParam: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: !Sub /uni-prop/${Stage}/UnicornWebNamespace - Value: !FindInMap [Constants, ServiceNamespace, Value] - # Services share their event bus name and arn UnicornWebEventBusParam: Type: AWS::SSM::Parameter @@ -285,7 +276,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" UnicornWebIngestDLQ: Type: AWS::SQS::Queue @@ -301,7 +292,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" ##### DYNAMODB WebTable: @@ -324,7 +315,7 @@ Resources: - Key: project Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace - Value: !FindInMap [Constants, ServiceNamespace, Value] + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" - Key: stage Value: !Ref Stage @@ -351,7 +342,7 @@ Resources: Condition: StringEquals: events:source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" # Catchall rule used for development purposes. UnicornWebCatchAllRule: @@ -369,7 +360,7 @@ Resources: account: - !Ref AWS::AccountId source: - - !FindInMap [Constants, ServiceNamespace, Value] + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornWebCatchAllLogGroup.Arn @@ -384,7 +375,7 @@ Resources: LogGroupName: !Sub - "/aws/events/${Stage}/${NS}-catchall" - Stage: !Ref Stage - NS: !FindInMap [Constants, ServiceNamespace, Value] + NS: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] # Permissions to allow EventBridge to send logs to CloudWatch From bf38a75562e81e2630c6884291c8a506a3b3a799 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 16:37:52 +1000 Subject: [PATCH 53/87] feat: Add shared stack to Python implementation --- unicorn_shared/samconfig.yaml | 18 ++++ unicorn_shared/uni-prop-shared.yaml | 136 ++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 unicorn_shared/samconfig.yaml create mode 100644 unicorn_shared/uni-prop-shared.yaml diff --git a/unicorn_shared/samconfig.yaml b/unicorn_shared/samconfig.yaml new file mode 100644 index 0000000..1afe5f0 --- /dev/null +++ b/unicorn_shared/samconfig.yaml @@ -0,0 +1,18 @@ +version: 0.1 + +default: + global: + parameters: + stack_name: uni-prop-local-shared + s3_prefix: uni-prop-local-shared + resolve_s3: true + deploy: + parameters: + template_file: uni-prop-shared.yaml + confirm_changeset: false + fail_on_empty_changeset: false + on_failure: ROLLBACK + capabilities: CAPABILITY_IAM + parameter_overrides: + - "Stage=Local" + diff --git a/unicorn_shared/uni-prop-shared.yaml b/unicorn_shared/uni-prop-shared.yaml new file mode 100644 index 0000000..75ca694 --- /dev/null +++ b/unicorn_shared/uni-prop-shared.yaml @@ -0,0 +1,136 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 +Description: > + Base infrastructure that will set up the central event bus and S3 image upload bucket. + + +#### PARAMETERS +Parameters: + Stage: + Type: String + Default: Local + AllowedValues: + - Local + - Dev + - Prod + + +#### MAPPINGS - Should be removed when values change to lower case +Mappings: + Stage: + Dev: + Value: dev + Prod: + Value: prod + Local: + Value: local + + +#### GLOBALS +Globals: + Function: + Timeout: 15 + Runtime: python3.11 + MemorySize: 128 + Tracing: Active + Architectures: + - arm64 + Tags: + stage: !Ref Stage + project: AWS Serverless Developer Experience + service: Unicorn Base Infrastructure + + +#### RESOURCES +Resources: + # S3 PROPERTY IMAGES BUCKET + UnicornPropertiesImagesBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub + - uni-prop-${st}-images-${AWS::AccountId} + - st: !FindInMap [Stage, !Ref Stage, Value] + + UnicornPropertiesImagesBucketParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /UniProp/${Stage}/ImagesBucket + Value: !Ref UnicornPropertiesImagesBucket + + # IMAGE UPLOAD CUSTOM RESOURCE FUNCTION + ImageUploadFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.lambda_handler + Runtime: python3.9 + Policies: + - S3CrudPolicy: + BucketName: !Ref UnicornPropertiesImagesBucket + - Statement: + - Sid: S3DeleteBucketPolicy + Effect: Allow + Action: + - s3:DeleteBucket + Resource: !GetAtt UnicornPropertiesImagesBucket.Arn + InlineCode: | + import os + import zipfile + from urllib.request import urlopen + import boto3 + import cfnresponse + + zip_file_name = 'property_images.zip' + url = f"https://aws-serverless-developer-experience-workshop-assets.s3.amazonaws.com/property_images/{zip_file_name}" + temp_zip_download_location = f"/tmp/{zip_file_name}" + + s3 = boto3.resource('s3') + + def create(event, context): + image_bucket_name = event['ResourceProperties']['DestinationBucket'] + bucket = s3.Bucket(image_bucket_name) + print(f"downloading zip file from: {url} to: {temp_zip_download_location}") + r = urlopen(url).read() + with open(temp_zip_download_location, 'wb') as t: + t.write(r) + print('zip file downloaded') + + print(f"unzipping file: {temp_zip_download_location}") + with zipfile.ZipFile(temp_zip_download_location,'r') as zip_ref: + zip_ref.extractall('/tmp') + + print('file unzipped') + + # upload to s3 + for root,_,files in os.walk('/tmp/property_images'): + for file in files: + print(f"file: {os.path.join(root, file)}") + print(f"s3 bucket: {image_bucket_name}") + bucket.upload_file(os.path.join(root, file), file) + def delete(event, context): + image_bucket_name = event['ResourceProperties']['DestinationBucket'] + img_bucket = s3.Bucket(image_bucket_name) + img_bucket.objects.delete() + img_bucket.delete() + def lambda_handler(event, context): + try: + if event['RequestType'] in ['Create', 'Update']: + create(event, context) + elif event['RequestType'] in ['Delete']: + delete(event, context) + except Exception as e: + print(e) + cfnresponse.send(event, context, cfnresponse.SUCCESS, dict()) + + ImageUpload: + Type: Custom::ImageUpload + Properties: + ServiceToken: !GetAtt ImageUploadFunction.Arn + DestinationBucket: !Ref UnicornPropertiesImagesBucket + +# OUTPUTS +Outputs: + ImageUploadBucketName: + Value: !Ref UnicornPropertiesImagesBucket From dd43f686fdb2791944935b133f952ee1762e079b Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:42:56 +1000 Subject: [PATCH 54/87] bug: Fix catch-all rule from each service to match events from every service source --- unicorn_contracts/template.yaml | 2 ++ unicorn_properties/template.yaml | 2 ++ unicorn_web/template.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index ec9c1ed..238877f 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -297,6 +297,8 @@ Resources: - !Ref AWS::AccountId source: - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornContractsCatchAllLogGroup.Arn diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 1ce1c1f..ed3e4b5 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -388,7 +388,9 @@ Resources: account: - !Ref AWS::AccountId source: + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: - Arn: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index d727f6b..b822839 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -360,6 +360,8 @@ Resources: account: - !Ref AWS::AccountId source: + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" State: ENABLED #You may want to disable this rule in production Targets: From e7ac110c255074b016be17f255e48355a0a33d4e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:46:34 +1000 Subject: [PATCH 55/87] chore: remove top-level section comments from templates --- unicorn_contracts/template.yaml | 7 ------- unicorn_properties/template.yaml | 7 ------- unicorn_web/template.yaml | 7 ------- 3 files changed, 21 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 238877f..ce5926d 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -7,7 +7,6 @@ Transform: Description: > Unicorn Contracts Service resources. -#### METADATA Metadata: cfn-lint: config: @@ -17,7 +16,6 @@ Metadata: - ES1001 - WS2001 -#### PARAMETERS Parameters: Stage: Type: String @@ -27,7 +25,6 @@ Parameters: - dev - prod -#### MAPPINGS Mappings: LogsRetentionPeriodMap: local: @@ -40,11 +37,9 @@ Mappings: ProjectName: Value: "AWS Serverless Developer Experience" -#### CONDITIONS Conditions: IsProd: !Equals [!Ref Stage, Prod] -#### GLOBALS Globals: Api: OpenApiVersion: 3.0.1 @@ -71,7 +66,6 @@ Globals: project: !FindInMap [Constants, ProjectName, Value] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" -#### RESOURCES Resources: #### SSM PARAMETERS @@ -411,7 +405,6 @@ Resources: - events:PutEvents Resource: !GetAtt UnicornContractsEventBus.Arn -#### OUTPUTS Outputs: BaseUrl: Description: Web service API endpoint diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index ed3e4b5..5a9ef31 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -5,7 +5,6 @@ Transform: AWS::Serverless-2016-10-31 Description: > Unicorn Properties Service resources -#### METADATA Metadata: cfn-lint: config: @@ -13,7 +12,6 @@ Metadata: - ES6000 - ES4000 -#### PARAMETERS Parameters: Stage: Type: String @@ -23,7 +21,6 @@ Parameters: - dev - prod -#### MAPPINGS Mappings: LogsRetentionPeriodMap: local: @@ -36,13 +33,11 @@ Mappings: ProjectName: Value: "AWS Serverless Developer Experience" -#### CONDITIONS Conditions: IsProd: !Equals - !Ref Stage - Prod -#### GLOBALS Globals: Function: Runtime: python3.11 @@ -67,7 +62,6 @@ Globals: project: !FindInMap [ Constants, ProjectName, Value ] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" -#### RESOURCES Resources: #### SSM PARAMETERS @@ -437,7 +431,6 @@ Resources: ] } -##### OUTPUTS Outputs: UnicornPropertiesEventBusName: Value: !GetAtt UnicornPropertiesEventBus.Name diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index b822839..4a11bcc 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -7,7 +7,6 @@ Transform: Description: > Unicorn Properties Services - web interface. Add, list and get details for Unicorn Properties. -#### METADATA Metadata: cfn-lint: config: @@ -19,7 +18,6 @@ Metadata: - ES1007 # - I3042 -#### PARAMETERS Parameters: Stage: Type: String @@ -29,7 +27,6 @@ Parameters: - dev - prod -#### MAPPINGS Mappings: LogsRetentionPeriodMap: local: @@ -42,11 +39,9 @@ Mappings: ProjectName: Value: "AWS Serverless Developer Experience" -#### CONDITIONS Conditions: IsProd: !Equals [!Ref Stage, Prod] -#### GLOBALS Globals: Api: OpenApiVersion: 3.0.1 @@ -73,7 +68,6 @@ Globals: project: !FindInMap [Constants, ProjectName, Value] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" -##### RESOURCES Resources: #### SSM PARAMETERS # Services share their event bus name and arn @@ -409,7 +403,6 @@ Resources: ] } -#### OUTPUTS Outputs: BaseUrl: Description: Web service API endpoint From a2f93eddfdf5c1f0a45819e608feff2ab5fd8047 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:47:53 +1000 Subject: [PATCH 56/87] chore: update Python dependencies --- unicorn_contracts/poetry.lock | 36 +++++--- unicorn_properties/poetry.lock | 104 +++------------------ unicorn_web/poetry.lock | 161 ++++++++++++++++----------------- 3 files changed, 111 insertions(+), 190 deletions(-) diff --git a/unicorn_contracts/poetry.lock b/unicorn_contracts/poetry.lock index 12c488c..4d84f14 100644 --- a/unicorn_contracts/poetry.lock +++ b/unicorn_contracts/poetry.lock @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.37" +version = "1.28.43" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.37-py3-none-any.whl", hash = "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6"}, - {file = "boto3-1.28.37.tar.gz", hash = "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2"}, + {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, + {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, ] [package.dependencies] -botocore = ">=1.31.37,<1.32.0" +botocore = ">=1.31.43,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.37" +version = "1.31.43" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.37-py3-none-any.whl", hash = "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06"}, - {file = "botocore-1.31.37.tar.gz", hash = "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de"}, + {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, + {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, ] [package.dependencies] @@ -444,13 +444,13 @@ files = [ [[package]] name = "moto" -version = "4.2.0" +version = "4.2.2" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.2.0-py2.py3-none-any.whl", hash = "sha256:e4835912f05627b6a53b938562b717122230fb038d023819133f8526f60ed0a7"}, - {file = "moto-4.2.0.tar.gz", hash = "sha256:00fbae396fc48c3596e47b4e3267c1a41ca01c968de023beb68e774c63910b58"}, + {file = "moto-4.2.2-py2.py3-none-any.whl", hash = "sha256:2a9cbcd9da1a66b23f95d62ef91968284445233a606b4de949379395056276fb"}, + {file = "moto-4.2.2.tar.gz", hash = "sha256:ee34c4c3f53900d953180946920c9dba127a483e2ed40e6dbf93d4ae2e760e7c"}, ] [package.dependencies] @@ -529,13 +529,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -590,6 +590,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -597,8 +598,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -615,6 +623,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -622,6 +631,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, diff --git a/unicorn_properties/poetry.lock b/unicorn_properties/poetry.lock index 5e15694..e6b3c98 100644 --- a/unicorn_properties/poetry.lock +++ b/unicorn_properties/poetry.lock @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.40" +version = "1.28.43" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.40-py3-none-any.whl", hash = "sha256:bd7c760afb195eaeaab907dc6b2c21fa64ddbba3fed4a869e80d820ddbd6cc70"}, - {file = "boto3-1.28.40.tar.gz", hash = "sha256:6ff9a5b815e106656596064d51c9b6ba97a307807baa5f89634384b7d3f7ecc6"}, + {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, + {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, ] [package.dependencies] -botocore = ">=1.31.40,<1.32.0" +botocore = ">=1.31.43,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.40" +version = "1.31.43" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.40-py3-none-any.whl", hash = "sha256:df766969f0d9ef9eda1a9c9946e0e173c10199f37a9e4c92861f11ddb5c9e702"}, - {file = "botocore-1.31.40.tar.gz", hash = "sha256:ce22a82ef8674f49691477d09558992cc87e7331f65c6a5b0da897ab192240ca"}, + {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, + {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, ] [package.dependencies] @@ -269,70 +269,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "coverage" -version = "7.3.0" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, - {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, - {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, - {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, - {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, - {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, - {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, - {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, - {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, - {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, - {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, - {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, - {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, - {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, -] - -[package.extras] -toml = ["tomli"] - [[package]] name = "cryptography" version = "41.0.3" @@ -593,13 +529,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, - {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -611,24 +547,6 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - [[package]] name = "pytest-mock" version = "3.11.1" @@ -956,4 +874,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "12fb02108f72c9c276aa75d575815d049503a519c1b2b3a920914592ca539b37" +content-hash = "345f217fecab7cc0c0bf492f2a3648386663ca363636a462579dba5c1fa55ca6" diff --git a/unicorn_web/poetry.lock b/unicorn_web/poetry.lock index 8ac5391..f0b5dc0 100644 --- a/unicorn_web/poetry.lock +++ b/unicorn_web/poetry.lock @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.37" +version = "1.28.43" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.37-py3-none-any.whl", hash = "sha256:709cf438ad3ea48d426e4659538fe1148fc2719469b52179d07a11c5d26abac6"}, - {file = "boto3-1.28.37.tar.gz", hash = "sha256:4aec1b54ba6cd352abba2cdd7cdc76e631a4d3ce79c55c0719f85f9c9842e4a2"}, + {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, + {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, ] [package.dependencies] -botocore = ">=1.31.37,<1.32.0" +botocore = ">=1.31.43,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.37" +version = "1.31.43" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.37-py3-none-any.whl", hash = "sha256:72e10759be3dff39c5eeb29f85c11a227c369c946d044f2caf62c352d6a6fc06"}, - {file = "botocore-1.31.37.tar.gz", hash = "sha256:5c92c8bc3c6b49950c95501b30f0ac551fd4952359b53a6fba243094028157de"}, + {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, + {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, ] [package.dependencies] @@ -271,63 +271,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.0" +version = "7.3.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, - {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, - {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, - {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, - {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, - {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, - {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, - {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, - {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, - {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, - {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, - {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, - {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, - {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, + {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, + {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, + {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, + {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, + {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, + {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, + {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, + {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, + {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, + {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, + {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, + {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, + {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, + {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, ] [package.extras] @@ -519,13 +519,13 @@ files = [ [[package]] name = "moto" -version = "4.2.0" +version = "4.2.2" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "moto-4.2.0-py2.py3-none-any.whl", hash = "sha256:e4835912f05627b6a53b938562b717122230fb038d023819133f8526f60ed0a7"}, - {file = "moto-4.2.0.tar.gz", hash = "sha256:00fbae396fc48c3596e47b4e3267c1a41ca01c968de023beb68e774c63910b58"}, + {file = "moto-4.2.2-py2.py3-none-any.whl", hash = "sha256:2a9cbcd9da1a66b23f95d62ef91968284445233a606b4de949379395056276fb"}, + {file = "moto-4.2.2.tar.gz", hash = "sha256:ee34c4c3f53900d953180946920c9dba127a483e2ed40e6dbf93d4ae2e760e7c"}, ] [package.dependencies] @@ -604,13 +604,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -640,23 +640,6 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytest-mock" -version = "3.11.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - [[package]] name = "python-dateutil" version = "2.8.2" @@ -683,6 +666,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -690,8 +674,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -708,6 +699,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -715,6 +707,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -957,4 +950,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "450945a735b7cd52c30f0204d9d6a45a8222696c8f8384334c04094d928d359c" +content-hash = "9e185582d06d620a83ded07aa755c7c7c8b268abff3ac879567f50aff7b0ec1f" From 5163fd8998dde5335fd9f06c1cea0f51c977978b Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:48:20 +1000 Subject: [PATCH 57/87] chore: remove unused PyTest Coverage tool --- unicorn_contracts/pyproject.toml | 2 -- unicorn_properties/pyproject.toml | 2 -- unicorn_web/pyproject.toml | 4 +--- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/unicorn_contracts/pyproject.toml b/unicorn_contracts/pyproject.toml index c25be68..d618de8 100644 --- a/unicorn_contracts/pyproject.toml +++ b/unicorn_contracts/pyproject.toml @@ -16,8 +16,6 @@ aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" pytest-mock = "^3.11.1" -# pytest-cov = "^4.1.0" -# coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index fd40c54..d35e76f 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -17,8 +17,6 @@ aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" pytest-mock = "^3.11.1" -pytest-cov = "^4.1.0" -coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index ea93a86..07003d2 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -18,9 +18,7 @@ crhelper = "^2.0.11" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" -pytest-mock = "^3.11.1" pytest-cov = "^4.1.0" -coverage = "^7.2.7" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" @@ -32,7 +30,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -vv -W ignore::UserWarning --cov=approvals_service --cov=search_service --cov-config=.coveragerc --cov-report term --cov-report html" +addopts = "-ra -vv -W ignore::UserWarning" testpaths = [ "./tests/unit", ] From c730205a6683c9d811d94cf82a564e601eeea5e9 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:51:30 +1000 Subject: [PATCH 58/87] chore: fix typos and misnomers in template comments --- unicorn_contracts/template.yaml | 5 ++--- unicorn_properties/template.yaml | 9 +++------ unicorn_web/template.yaml | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index ce5926d..ffdc432 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -67,7 +67,6 @@ Globals: namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" Resources: - #### SSM PARAMETERS # Services share their event bus name and arn UnicornContractsEventBusNameParam: @@ -255,13 +254,13 @@ Resources: Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" #### EVENT BUS - # Event bus for Contract Service used to publish and consume events + # Event bus for Unicorn Contract Service used to publish and consume events UnicornContractsEventBus: Type: AWS::Events::EventBus Properties: Name: !Sub UnicornContractsBus-${Stage} - # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) + # Event bus policy to restrict who can publish events (should only be services from UnicornContractsNamespace) ContractEventsBusPublishPolicy: Type: AWS::Events::EventBusPolicy Properties: diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 5a9ef31..4089437 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -63,7 +63,6 @@ Globals: namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" Resources: - #### SSM PARAMETERS # Services share their event bus name and arn UnicornPropertiesEventBusNameParam: @@ -344,16 +343,14 @@ Resources: - Key: stage Value: !Ref Stage - -#### EVENT BUS - - # Event bus for Contract Service used to publish and consume events + #### EVENT BUS + # Event bus for Unicorn Properties Service used to publish and consume events UnicornPropertiesEventBus: Type: AWS::Events::EventBus Properties: Name: !Sub UnicornPropertiesBus-${Stage} - # Event bus policy to restrict who can publish events (should only be services from ServiceNamespace) + # Event bus policy to restrict who can publish events (should only be services from UnicornPropertiesNamespace) UnicornPropertiesEventsBusPublishPolicy: Type: AWS::Events::EventBusPolicy Properties: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 4a11bcc..fefc2b7 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -314,13 +314,13 @@ Resources: Value: !Ref Stage #### EVENT BUS - # Event bus for Properties Web used to publish and consume events + # Event bus for Unicorn Web Service used to publish and consume events UnicornWebEventBus: Type: AWS::Events::EventBus Properties: Name: !Sub UnicornWebBus-${Stage} - # Event bus policy to restrict who can publish events (should only be services from WebServiceNamespace) + # Event bus policy to restrict who can publish events (should only be services from UnicornWebNamespace) UnicornWebEventBusPublishPolicy: Type: AWS::Events::EventBusPolicy Properties: From 2b0f150c9e85e409e78791cf8a736f5067989865 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 17:53:02 +1000 Subject: [PATCH 59/87] chore: add description comments to template resources --- unicorn_contracts/template.yaml | 12 ++++++++++-- unicorn_properties/template.yaml | 2 ++ unicorn_web/template.yaml | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index ffdc432..f2adf67 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -84,6 +84,7 @@ Resources: Value: !GetAtt UnicornContractsEventBus.Arn #### LAMBDA FUNCTIONS + # Processes customer API requests from SQS queue UnicornContractsIngestQueue ContractEventHandlerFunction: Type: AWS::Serverless::Function Properties: @@ -173,7 +174,7 @@ Resources: - sqs:GetQueueUrl Resource: !GetAtt UnicornContractsIngestQueue.Arn - # API GW Cloudwatch Log Group + # API GW CloudWatch Logs Group, logs all requests from API Gateway UnicornContractsApiLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -194,6 +195,7 @@ Resources: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs #### INGEST QUEUES + # Queue API Gateway requests to be processed by ContractEventHandlerFunction UnicornContractsIngestQueue: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete @@ -213,7 +215,7 @@ Resources: Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" - + # DeadLetterQueue for UnicornContractsIngestQueue. Contains messages that failed to be processed UnicornContractsIngestDLQ: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete @@ -231,6 +233,7 @@ Resources: Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" #### DYNAMODB TABLE + # Persist Contracts information in DynamoDB ContractsTable: Type: AWS::DynamoDB::Table UpdateReplacePolicy: Delete @@ -405,6 +408,7 @@ Resources: Resource: !GetAtt UnicornContractsEventBus.Arn Outputs: + #### API GATEWAY OUTPUTS BaseUrl: Description: Web service API endpoint Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" @@ -412,18 +416,22 @@ Outputs: Description: Contract service API endpoint Value: !Sub "https://${UnicornContractsApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + #### SQS OUTPUTS IngestQueueUrl: Description: URL for the Ingest SQS Queue Value: !GetAtt UnicornContractsIngestQueue.QueueUrl + #### DYNAMODB OUTPUTS ContractsTableName: Description: DynamoDB table storing contract information Value: !Ref ContractsTable ContractEventHandlerFunction: Description: Create Contract function ARN + #### LAMBDA FUNCTIONS OUTPUTS Value: !GetAtt ContractEventHandlerFunction.Arn + #### CLOUDWATCH LOGS OUTPUTS UnicornContractsCatchAllLogGroupArn: Value: !GetAtt UnicornContractsCatchAllLogGroup.Arn diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 4089437..b5e0cd3 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -432,9 +432,11 @@ Outputs: UnicornPropertiesEventBusName: Value: !GetAtt UnicornPropertiesEventBus.Name + #### DYNAMODB OUTPUTS ContractStatusTableName: Value: !Ref ContractStatusTable + #### LAMBDA FUNCTIONS OUTPUTS ContractStatusChangedHandlerFunctionName: Value: !Ref ContractStatusChangedHandlerFunction ContractStatusChangedHandlerFunctionArn: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index fefc2b7..d6dc5eb 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -86,6 +86,7 @@ Resources: Value: !GetAtt UnicornWebEventBus.Arn ##### LAMBDA FUNCTIONS + # Handle Search and Property details requests from API SearchFunction: Type: AWS::Serverless::Function Properties: @@ -103,6 +104,7 @@ Resources: LogGroupName: !Sub "/aws/lambda/${SearchFunction}" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Process queued API requests to approve properties from UnicornWebIngestQueue RequestApprovalFunction: Type: AWS::Serverless::Function Properties: @@ -131,6 +133,7 @@ Resources: LogGroupName: !Sub "/aws/lambda/${RequestApprovalFunction}" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Respond to PublicationEvaluationCompleted events from Unicorn Web EventBus PublicationApprovedEventHandlerFunction: Type: AWS::Serverless::Function Properties: @@ -243,7 +246,7 @@ Resources: - lambda:InvokeFunction Resource: !GetAtt SearchFunction.Arn - # API GW Cloudwatch Log Group + # API GW CloudWatch Logs Group, logs all requests from API Gateway UnicornWebApiLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -252,6 +255,7 @@ Resources: RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### INGEST QUEUES + # Queue API Gateway requests to be processed by RequestApprovalFunction UnicornWebIngestQueue: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete @@ -271,7 +275,7 @@ Resources: Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" - + # DeadLetterQueue for UnicornWebIngestQueue. Contains messages that failed to be processed UnicornWebIngestDLQ: Type: AWS::SQS::Queue UpdateReplacePolicy: Delete @@ -289,6 +293,7 @@ Resources: Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" ##### DYNAMODB + # Persists Property details in DynamoDB WebTable: Type: AWS::DynamoDB::Table UpdateReplacePolicy: Delete From bdc896b2290eaa4d1bab412a078f11758f7a1180 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:33:40 +1000 Subject: [PATCH 60/87] chore: add comments to resources and cfn-lint rules in template.yaml --- unicorn_contracts/template.yaml | 16 +++++++++++----- unicorn_properties/template.yaml | 24 ++++++++++++++++++------ unicorn_web/template.yaml | 12 +++++------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index f2adf67..def48b0 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -5,16 +5,16 @@ Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: > - Unicorn Contracts Service resources. + Unicorn Contracts Service. Manage contract information for property listings. Metadata: cfn-lint: config: ignore_checks: - - ES4000 - - ES6000 - - ES1001 - - WS2001 + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - WS2001 # Rule disabled because check does not support !ToJsonString transform + - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure Parameters: Stage: @@ -83,6 +83,7 @@ Resources: Name: !Sub /uni-prop/${Stage}/UnicornContractsEventBusArn Value: !GetAtt UnicornContractsEventBus.Arn + #### LAMBDA FUNCTIONS # Processes customer API requests from SQS queue UnicornContractsIngestQueue ContractEventHandlerFunction: @@ -194,6 +195,7 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + #### INGEST QUEUES # Queue API Gateway requests to be processed by ContractEventHandlerFunction UnicornContractsIngestQueue: @@ -215,6 +217,8 @@ Resources: Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + + #### DEAD LETTER QUEUES # DeadLetterQueue for UnicornContractsIngestQueue. Contains messages that failed to be processed UnicornContractsIngestDLQ: Type: AWS::SQS::Queue @@ -256,6 +260,7 @@ Resources: - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + #### EVENT BUS # Event bus for Unicorn Contract Service used to publish and consume events UnicornContractsEventBus: @@ -344,6 +349,7 @@ Resources: ] } + #### EVENT BRIDGE PIPES # Pipe changed on DynamoDB Table to UnicornContractsEventBus DdbStreamToEventPipe: diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index b5e0cd3..7339caa 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -1,16 +1,18 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 Description: > - Unicorn Properties Service resources + Unicorn Properties Service. Validate the content, images and contract of property listings. Metadata: cfn-lint: config: ignore_checks: - - ES6000 - - ES4000 + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy Parameters: Stage: @@ -81,6 +83,7 @@ Resources: #### LAMBDA FUNCTIONS + # Listens to ContractStatusChanged events from EventBridge ContractStatusChangedHandlerFunction: Type: AWS::Serverless::Function Properties: @@ -113,6 +116,7 @@ Resources: Type: SQS Destination: !GetAtt PropertiesServiceDLQ.Arn + # Listens to Contract status changes from ContractStatusTable to un-pause StepFunctions PropertiesApprovalSyncFunction: Type: AWS::Serverless::Function Properties: @@ -150,6 +154,7 @@ Resources: Type: SQS Destination: !GetAtt PropertiesServiceDLQ.Arn + # Part of the ApprovalStateMachine, checks if a given Property has an existing Contract in ContractStatusTable ContractExistsCheckerFunction: Type: AWS::Serverless::Function Properties: @@ -159,12 +164,14 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable + # Part of the ApprovalStateMachine, validates if all outputs of content checking steps are OK ContentIntegrityValidatorFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/ Handler: properties_service.content_integrity_validator_function.lambda_handler + # Part of the ApprovalStateMachine, pauses the workflow execution and stores token in ContractStatusTable until contract is approved WaitForContractApprovalFunction: Type: AWS::Serverless::Function Properties: @@ -272,6 +279,7 @@ Resources: ServiceName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" #### CLOUDWATCH LOG GROUPS + # Store Lambda execution logs for each Lambda function in Unicorn Properties Service ContractStatusChangedHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -312,6 +320,7 @@ Resources: LogGroupName: !Sub "/aws/lambda/${WaitForContractApprovalFunction}" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Store ApprovalStateMachineLogGroup workflow execution logs ApprovalStateMachineLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -320,6 +329,7 @@ Resources: LogGroupName: !Sub "/aws/states/${AWS::StackName}-ApprovalStateMachine" RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + #### DYNAMODB TABLE ContractStatusTable: Type: AWS::DynamoDB::Table @@ -343,8 +353,9 @@ Resources: - Key: stage Value: !Ref Stage + #### EVENT BUS - # Event bus for Unicorn Properties Service used to publish and consume events + # Event bus for Unicorn Properties Service, used to publish and consume events UnicornPropertiesEventBus: Type: AWS::Events::EventBus Properties: @@ -368,7 +379,7 @@ Resources: events:source: - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - # Catchall rule used for development purposes. + # Catchall rule used for development purposes. Logs all events matching any of the services' to CloudWatch Logs UnicornPropertiesCatchAllRule: Type: AWS::Events::Rule Properties: @@ -434,6 +445,7 @@ Outputs: #### DYNAMODB OUTPUTS ContractStatusTableName: + Description: DynamoDB table storing contract status information Value: !Ref ContractStatusTable #### LAMBDA FUNCTIONS OUTPUTS diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index d6dc5eb..6920865 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -5,18 +5,16 @@ Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: > - Unicorn Properties Services - web interface. Add, list and get details for Unicorn Properties. + Unicorn Web Service - web interface. Add, list and get details for Unicorn Properties. Metadata: cfn-lint: config: ignore_checks: - - WS2001 - - ES6000 - - ES4000 - - ES1001 - - ES1007 - # - I3042 + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - WS2001 # Rule disabled because check does not support !ToJsonString transform + - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure Parameters: Stage: From 788c9436c430d8a03fec562975a918254a3c51e0 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:36:07 +1000 Subject: [PATCH 61/87] chore: reordered resources in template.yaml to improve readability --- unicorn_contracts/template.yaml | 35 +++++++++-------- unicorn_properties/template.yaml | 65 +++++++++++++++++--------------- unicorn_web/template.yaml | 25 ++++++------ 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index def48b0..3bd6990 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -106,13 +106,6 @@ Resources: ScalingConfig: MaximumConcurrency: 5 - ContractEventHandlerFunctionLogGroup: - Type: AWS::Logs::LogGroup - DeletionPolicy: Delete - UpdateReplacePolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### API GATEWAY REST API UnicornContractsApi: @@ -175,14 +168,6 @@ Resources: - sqs:GetQueueUrl Resource: !GetAtt UnicornContractsIngestQueue.Arn - # API GW CloudWatch Logs Group, logs all requests from API Gateway - UnicornContractsApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - UnicornContractsApiGwAccountConfigRole: Type: AWS::IAM::Role Properties: @@ -236,6 +221,26 @@ Resources: - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + + #### CLOUDWATCH LOG GROUPS + # Logs all invocations of ContractEventHandler Function + ContractEventHandlerFunctionLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + # API GW CloudWatch Logs Group, logs all requests from API Gateway + UnicornContractsApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + #### DYNAMODB TABLE # Persist Contracts information in DynamoDB ContractsTable: diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 7339caa..81e6562 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -181,36 +181,6 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable - #### DLQs - PropertiesEventBusRuleDLQ: - Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - Tags: - - Key: project - Value: !FindInMap [ Constants, ProjectName, Value ] - - Key: namespace - Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - - Key: stage - Value: !Ref Stage - - PropertiesServiceDLQ: - Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - SqsManagedSseEnabled: true - MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) - Tags: - - Key: project - Value: !FindInMap [ Constants, ProjectName, Value ] - - Key: namespace - Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - - Key: stage - Value: !Ref Stage #### STATE MACHINE ApprovalStateMachine: @@ -278,6 +248,41 @@ Resources: EventBusName: !GetAtt UnicornPropertiesEventBus.Name ServiceName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + + #### DEAD LETTER QUEUES + # Store EventBridge events that failed to be DELIVERED to ContractStatusChangedHandlerFunction + PropertiesEventBusRuleDLQ: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + Tags: + - Key: project + Value: !FindInMap [ Constants, ProjectName, Value ] + - Key: namespace + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + - Key: stage + Value: !Ref Stage + + # Store failed INVOCATIONS to each Lambda function in Unicorn Properties Service + PropertiesServiceDLQ: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) + Tags: + - Key: project + Value: !FindInMap [ Constants, ProjectName, Value ] + - Key: namespace + Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + - Key: stage + Value: !Ref Stage + + #### CLOUDWATCH LOG GROUPS # Store Lambda execution logs for each Lambda function in Unicorn Properties Service ContractStatusChangedHandlerFunctionLogGroup: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 6920865..d7210d8 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -414,6 +414,19 @@ Outputs: Description: Web service API endpoint Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + ApiSearchPropertiesByCity: + Description: "GET request to list all properties in a given city" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}" + ApiSearchPropertiesByStreet: + Description: "GET request to list all properties in a given street" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}/{street}" + ApiPropertyApproval: + Description: "POST request to add a property to the database" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/request_approval" + ApiPropertyDetails: + Description: "GET request to get the full details of a single property" + Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/properties/{country}/{city}/{street}/{number}" + IngestQueueUrl: Description: URL for the Ingest SQS Queue Value: !GetAtt UnicornWebIngestQueue.QueueUrl @@ -436,15 +449,3 @@ Outputs: Description: Is Production? Value: !If [IsProd, "true", "false"] - ApiSearchPropertiesByCity: - Description: "GET request to list all properties in a given city" - Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}" - ApiSearchPropertiesByStreet: - Description: "GET request to list all properties in a given street" - Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}/{street}" - ApiPropertyApproval: - Description: "POST request to add a property to the database" - Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/request_approval" - ApiPropertyDetails: - Description: "GET request to get the full details of a single property" - Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/properties/{country}/{city}/{street}/{number}" From a7799b2b0e1ea8f8b68f2649337129e25a3d7569 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:36:54 +1000 Subject: [PATCH 62/87] chore: removed unused outputs --- unicorn_contracts/template.yaml | 4 ---- unicorn_properties/template.yaml | 3 --- unicorn_web/template.yaml | 4 ---- 3 files changed, 11 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 3bd6990..6330b78 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -445,7 +445,3 @@ Outputs: #### CLOUDWATCH LOGS OUTPUTS UnicornContractsCatchAllLogGroupArn: Value: !GetAtt UnicornContractsCatchAllLogGroup.Arn - - IsProd: - Description: Is Production? - Value: !If [IsProd, "true", "false"] diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 81e6562..d9865c0 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -445,9 +445,6 @@ Resources: } Outputs: - UnicornPropertiesEventBusName: - Value: !GetAtt UnicornPropertiesEventBus.Name - #### DYNAMODB OUTPUTS ContractStatusTableName: Description: DynamoDB table storing contract status information diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index d7210d8..56b0ed8 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -445,7 +445,3 @@ Outputs: Description: Publication evaluation event handler function ARN Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn - IsProd: - Description: Is Production? - Value: !If [IsProd, "true", "false"] - From 8239d5e8319ec33cdedf8778482a8762849156bd Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:37:52 +1000 Subject: [PATCH 63/87] chore: lambda function outputs - added ARN output, renamed Name output --- unicorn_contracts/template.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 6330b78..8b695f3 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -437,9 +437,12 @@ Outputs: Description: DynamoDB table storing contract information Value: !Ref ContractsTable - ContractEventHandlerFunction: - Description: Create Contract function ARN #### LAMBDA FUNCTIONS OUTPUTS + ContractEventHandlerFunctionName: + Description: ContractEventHandler function name + Value: !Ref ContractEventHandlerFunction + ContractEventHandlerFunctionArn: + Description: ContractEventHandler function ARN Value: !GetAtt ContractEventHandlerFunction.Arn #### CLOUDWATCH LOGS OUTPUTS From 62f3684ca10de70141614b4e9e9fd19e8814bcfc Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:38:30 +1000 Subject: [PATCH 64/87] chore: reordered and fixed StateMachine policy templates --- unicorn_properties/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index d9865c0..420abd7 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -192,6 +192,8 @@ Resources: Enabled: true Policies: - AWSXRayDaemonWriteAccess + - ComprehendBasicAccessPolicy + - RekognitionDetectOnlyPolicy - LambdaInvokePolicy: FunctionName: !Ref WaitForContractApprovalFunction - LambdaInvokePolicy: @@ -200,8 +202,6 @@ Resources: FunctionName: !Ref ContractExistsCheckerFunction - S3ReadPolicy: BucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}" - - ComprehendBasicAccessPolicy: {} - - RekognitionDetectOnlyPolicy: {} - EventBridgePutEventsPolicy: EventBusName: !GetAtt UnicornPropertiesEventBus.Name - Statement: From dd57e7d8122a37e1247bc07a6fec28cd90e140ef Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:39:27 +1000 Subject: [PATCH 65/87] chore: Uncommented StateMachine event trigger, as dependency with unicorn_web Namespace SSM param was removed --- unicorn_properties/template.yaml | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 420abd7..ca5ea3b 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -223,23 +223,23 @@ Resources: LogGroupArn: !GetAtt ApprovalStateMachineLogGroup.Arn Level: ALL IncludeExecutionData: true - # Events: - # PubApproReqEvent: - # Type: EventBridgeRule - # Properties: - # RuleName: properties.pubapprovalwf-web.pubapprovalrequested - # EventBusName: !GetAtt UnicornPropertiesEventBus.Name - # Pattern: - # source: - # - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" - # detail-type: - # - PublicationApprovalRequested - # RetryPolicy: - # MaximumRetryAttempts: 5 - # MaximumEventAgeInSeconds: 900 - # DeadLetterConfig: - # Type: SQS - # Destination: !GetAtt PropertiesServiceDLQ.Arn + Events: + PubApproReqEvent: + Type: EventBridgeRule + Properties: + RuleName: properties.pubapprovalwf-web.pubapprovalrequested + EventBusName: !GetAtt UnicornPropertiesEventBus.Name + Pattern: + source: + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + detail-type: + - PublicationApprovalRequested + RetryPolicy: + MaximumRetryAttempts: 5 + MaximumEventAgeInSeconds: 900 + DeadLetterConfig: + Type: SQS + Destination: !GetAtt PropertiesServiceDLQ.Arn DefinitionSubstitutions: ContractExistsChecker: !GetAtt ContractExistsCheckerFunction.Arn WaitForContractApproval: !GetAtt WaitForContractApprovalFunction.Arn From c3c74527b20d765475d2259c76bc1023619fe7ae Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:39:56 +1000 Subject: [PATCH 66/87] chore: add missing Outputs for CatchAll CW Logs group --- unicorn_properties/template.yaml | 4 ++++ unicorn_web/template.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index ca5ea3b..3232480 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -475,3 +475,7 @@ Outputs: Value: !Ref WaitForContractApprovalFunction WaitForContractApprovalFunctionArn: Value: !GetAtt WaitForContractApprovalFunction.Arn + + #### CLOUDWATCH LOGS OUTPUTS + UnicornPropertiesCatchAllLogGroupArn: + Value: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 56b0ed8..6f6c41e 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -445,3 +445,5 @@ Outputs: Description: Publication evaluation event handler function ARN Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn + UnicornWebCatchAllLogGroupArn: + Value: !GetAtt UnicornWebCatchAllLogGroup.Arn From 7897190d84e76a41ab8e119fb51da65f50b90f94 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:40:17 +1000 Subject: [PATCH 67/87] chore: add missing OnFailure SQS destination --- unicorn_web/template.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 6f6c41e..031b2ca 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -140,6 +140,10 @@ Resources: Policies: - DynamoDBWritePolicy: TableName: !Ref WebTable + EventInvokeConfig: + DestinationConfig: + OnFailure: + Type: SQS Events: ApprovalEvent: Type: EventBridgeRule From c87589f540ac0b64f1eb0ee0b23e5eea81657a12 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:41:31 +1000 Subject: [PATCH 68/87] bug: replaces status=NEW for PENDING on test DDB items, as per new architecture --- unicorn_web/tests/unit/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_web/tests/unit/helper.py b/unicorn_web/tests/unit/helper.py index a195a6b..9e73922 100644 --- a/unicorn_web/tests/unit/helper.py +++ b/unicorn_web/tests/unit/helper.py @@ -66,7 +66,7 @@ def create_ddb_table_property_web(dynamodb): 'listprice': '200', 'currency': 'USD', 'images': [], - 'status': 'NEW', + 'status': 'PENDING', }) table.put_item(Item={ 'PK': 'PROPERTY#usa#anytown', From ff004ff81874dcfd38a8cc137fa3c56030e38b68 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Fri, 8 Sep 2023 18:41:53 +1000 Subject: [PATCH 69/87] chore: remove commented-out Event payload --- unicorn_web/tests/unit/event_generator.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/unicorn_web/tests/unit/event_generator.py b/unicorn_web/tests/unit/event_generator.py index 334380c..66eff96 100644 --- a/unicorn_web/tests/unit/event_generator.py +++ b/unicorn_web/tests/unit/event_generator.py @@ -152,26 +152,3 @@ def sqs_event(messages: List[dict], }) return SQSEvent({ "Records": records }) - - - - -# { -# "Records": [ -# { -# "messageAttributes": { -# "HttpMethod": { -# "stringValue": "PUT", -# "stringListValues": [], -# "binaryListValues": [], -# "dataType": "String" -# } -# }, -# "md5OfMessageAttributes": "39c36267fdf9c8d354b1069e44662d24", -# "md5OfBody": "4bc398b7ae8e52d7f7eaed3cb76c12ef", -# "eventSource": "aws:sqs", -# "eventSourceARN": "arn:aws:sqs:ap-southeast-2:718758479978:uni-prop-local-contract-UnicornContractsIngestQueue-p5zisiK0Xbxn", -# "awsRegion": "ap-southeast-2" -# } -# ] -# } \ No newline at end of file From 46f01b1c0223b450ee9c0d36cb5bfb7e4cc07308 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 11 Sep 2023 08:49:24 +0800 Subject: [PATCH 70/87] chore: minor changes to template formatting (#39) * chore: minor changes to template formatting * chore: additional template updates and changes to subscriptions * chore: additional template updates and changes to subscriptions * fix: updated rule name * fix: updated rule names --- .../integration/subscriber-policies.yaml | 5 +- unicorn_contracts/template.yaml | 39 +++--- .../integration/subscriber-policies.yaml | 2 +- .../integration/subscriptions.yaml | 47 ++----- unicorn_properties/template.yaml | 117 +++++++++--------- .../integration/subscriber-policies.yaml | 2 +- unicorn_web/integration/subscriptions.yaml | 12 +- unicorn_web/template.yaml | 6 +- 8 files changed, 103 insertions(+), 127 deletions(-) diff --git a/unicorn_contracts/integration/subscriber-policies.yaml b/unicorn_contracts/integration/subscriber-policies.yaml index 1b0cc0a..929d627 100644 --- a/unicorn_contracts/integration/subscriber-policies.yaml +++ b/unicorn_contracts/integration/subscriber-policies.yaml @@ -15,12 +15,13 @@ Parameters: - prod Resources: - # Update this policy as you get new subscribers by adding their namespace to events:source + # This policy defines who can create rules on the event bus. Only principals subscribing to + # Contracts Service events can create rule on the bus. No rules without a defined source. CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" - StatementId: !Sub "${AWS::StackName}-CreateRule" + StatementId: !Sub "OnlyRulesForContractServiceEvents-${Stage}" Statement: Effect: Allow Principal: diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 8b695f3..1cb7085 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -53,7 +53,7 @@ Globals: Environment: Variables: DYNAMODB_TABLE: !Ref ContractsTable - SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" POWERTOOLS_LOGGER_CASE: PascalCase POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default @@ -106,6 +106,13 @@ Resources: ScalingConfig: MaximumConcurrency: 5 + ContractEventHandlerFunctionLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### API GATEWAY REST API UnicornContractsApi: @@ -168,6 +175,14 @@ Resources: - sqs:GetQueueUrl Resource: !GetAtt UnicornContractsIngestQueue.Arn + # API GW Cloudwatch Log Group + UnicornContractsApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + UnicornContractsApiGwAccountConfigRole: Type: AWS::IAM::Role Properties: @@ -221,26 +236,6 @@ Resources: - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" - - #### CLOUDWATCH LOG GROUPS - # Logs all invocations of ContractEventHandler Function - ContractEventHandlerFunctionLogGroup: - Type: AWS::Logs::LogGroup - DeletionPolicy: Delete - UpdateReplacePolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContractEventHandlerFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - # API GW CloudWatch Logs Group, logs all requests from API Gateway - UnicornContractsApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - #### DYNAMODB TABLE # Persist Contracts information in DynamoDB ContractsTable: @@ -278,7 +273,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Ref UnicornContractsEventBus - StatementId: !Sub ContactsPublishEventsPolicy-${Stage} + StatementId: !Sub OnlyContactsServiceCanPublishToEventBus-${Stage} Statement: Effect: Allow Principal: diff --git a/unicorn_properties/integration/subscriber-policies.yaml b/unicorn_properties/integration/subscriber-policies.yaml index 469d548..32d3441 100644 --- a/unicorn_properties/integration/subscriber-policies.yaml +++ b/unicorn_properties/integration/subscriber-policies.yaml @@ -20,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" - StatementId: !Sub "${AWS::StackName}-CreateRule" + StatementId: !Sub "OnlyRulesForPropertiesServiceEvents-${Stage}" Statement: Effect: Allow Principal: diff --git a/unicorn_properties/integration/subscriptions.yaml b/unicorn_properties/integration/subscriptions.yaml index 4b2a64f..0536820 100644 --- a/unicorn_properties/integration/subscriptions.yaml +++ b/unicorn_properties/integration/subscriptions.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the cross-service subscriptions for Unicorn Properties +Description: Defines the rule for the events (subscriptions) that Unicorn Properties wants to consume. Parameters: Stage: @@ -13,11 +13,13 @@ Parameters: - prod Resources: + + #### UNICORN CONTRACTS EVENT SUBSCRIPTIONS ContractStatusChangedSubscriptionRule: Type: AWS::Events::Rule Properties: - Name: properties.statuschanged-contract.pubevalcompleted - Description: Constract Status Changed subscription + Name: unicorn.properties-ContractStatusChanged + Description: Contract Status Changed subscription EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" EventPattern: source: @@ -28,35 +30,14 @@ Resources: Targets: - Id: SendEventTo Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" - RoleArn: !GetAtt UnicornContractsEventBusToUnicornPropertiesEventBusRole.Arn - - # This IAM role allows EventBridge to assume the permissions necessary to send events - # from the Unicorn Contracts event bus to the Unicorn Properties event bus. - # If the Unicorn Properties service wants receive events from other services, then add another policy - # the arn of the which includes the event bus arn where Unicorn Properties is creating the - # subscription (rule) - UnicornContractsEventBusToUnicornPropertiesEventBusRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Principal: - Service: events.amazonaws.com - Policies: - - PolicyName: PutEventsOnUnicornPropertiesEventBus - PolicyDocument: - Statement: - - Effect: Allow - Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + RoleArn: !GetAtt UnicornPropertiesSubscriptionRole.Arn + #### UNICORN WEB EVENT SUBSCRIPTIONS PublicationApprovalRequestedSubscriptionRule: Type: AWS::Events::Rule Properties: - Name: properties.pubapprovalwf-web.pubapprovalrequested - Description: Publication Approval Requested Subscription + Name: unicorn.properties-PublicationApprovalRequested + Description: Publication evaluation completed subscription EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" EventPattern: source: @@ -67,14 +48,12 @@ Resources: Targets: - Id: SendEventTo Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" - RoleArn: !GetAtt UnicornWebEventBusToUnicornPropertiesEventBusRole.Arn + RoleArn: !GetAtt UnicornPropertiesSubscriptionRole.Arn + # This IAM role allows EventBridge to assume the permissions necessary to send events - # from the Unicorn Web event bus to the Unicorn Properties event bus. - # If the Unicorn Properties service wants receive events from other services, then add another policy - # the arn of the which includes the event bus arn where Unicorn Properties is creating the - # subscription (rule) - UnicornWebEventBusToUnicornPropertiesEventBusRole: + # from the publishing event bus, to the subscribing event bus (UnicornPropertiesEventBusArn) + UnicornPropertiesSubscriptionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 3232480..2e8100f 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -98,7 +98,7 @@ Resources: StatusChangedEvent: Type: EventBridgeRule Properties: - RuleName: properties.contstatuschangedhdr-contracts.contstatuschanged + RuleName: unicorn.properties-ContractStatusChanged EventBusName: !GetAtt UnicornPropertiesEventBus.Name Pattern: source: @@ -116,6 +116,15 @@ Resources: Type: SQS Destination: !GetAtt PropertiesServiceDLQ.Arn + # Log group for the ContractStatusChangedHandlerFunction + ContractStatusChangedHandlerFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractStatusChangedHandlerFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Listens to Contract status changes from ContractStatusTable to un-pause StepFunctions PropertiesApprovalSyncFunction: Type: AWS::Serverless::Function @@ -154,6 +163,15 @@ Resources: Type: SQS Destination: !GetAtt PropertiesServiceDLQ.Arn + # Log group for the PropertiesApprovalSyncFunction + PropertiesApprovalSyncFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${PropertiesApprovalSyncFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Part of the ApprovalStateMachine, checks if a given Property has an existing Contract in ContractStatusTable ContractExistsCheckerFunction: Type: AWS::Serverless::Function @@ -164,6 +182,15 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable + # Log group for the ContractExistsCheckerFunction + ContractExistsCheckerFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContractExistsCheckerFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Part of the ApprovalStateMachine, validates if all outputs of content checking steps are OK ContentIntegrityValidatorFunction: Type: AWS::Serverless::Function @@ -171,6 +198,15 @@ Resources: CodeUri: src/ Handler: properties_service.content_integrity_validator_function.lambda_handler + # Log group for the ContentIntegrityValidatorFunction + ContentIntegrityValidatorFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${ContentIntegrityValidatorFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + # Part of the ApprovalStateMachine, pauses the workflow execution and stores token in ContractStatusTable until contract is approved WaitForContractApprovalFunction: Type: AWS::Serverless::Function @@ -181,6 +217,14 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable + # Log group for the WaitForContractApprovalFunction + WaitForContractApprovalFunctionLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/lambda/${WaitForContractApprovalFunction}" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### STATE MACHINE ApprovalStateMachine: @@ -227,7 +271,7 @@ Resources: PubApproReqEvent: Type: EventBridgeRule Properties: - RuleName: properties.pubapprovalwf-web.pubapprovalrequested + RuleName: unicorn.properties-PublicationApprovalRequested EventBusName: !GetAtt UnicornPropertiesEventBus.Name Pattern: source: @@ -248,6 +292,14 @@ Resources: EventBusName: !GetAtt UnicornPropertiesEventBus.Name ServiceName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + # Store ApprovalStateMachineLogGroup workflow execution logs + ApprovalStateMachineLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + LogGroupName: !Sub "/aws/states/${AWS::StackName}-ApprovalStateMachine" + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] #### DEAD LETTER QUEUES # Store EventBridge events that failed to be DELIVERED to ContractStatusChangedHandlerFunction @@ -282,59 +334,6 @@ Resources: - Key: stage Value: !Ref Stage - - #### CLOUDWATCH LOG GROUPS - # Store Lambda execution logs for each Lambda function in Unicorn Properties Service - ContractStatusChangedHandlerFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContractStatusChangedHandlerFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - PropertiesApprovalSyncFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${PropertiesApprovalSyncFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - ContractExistsCheckerFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContractExistsCheckerFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - ContentIntegrityValidatorFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${ContentIntegrityValidatorFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - WaitForContractApprovalFunctionLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/lambda/${WaitForContractApprovalFunction}" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - # Store ApprovalStateMachineLogGroup workflow execution logs - ApprovalStateMachineLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - LogGroupName: !Sub "/aws/states/${AWS::StackName}-ApprovalStateMachine" - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - #### DYNAMODB TABLE ContractStatusTable: Type: AWS::DynamoDB::Table @@ -371,7 +370,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Ref UnicornPropertiesEventBus - StatementId: !Sub PropertiesPublishEventsPolicy-${Stage} + StatementId: !Sub OnlyPropertiesServiceCanPublishToEventBus-${Stage} Statement: Effect: Allow Principal: @@ -384,12 +383,12 @@ Resources: events:source: - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - # Catchall rule used for development purposes. Logs all events matching any of the services' to CloudWatch Logs + # Catchall rule used for development purposes. Logs all events matching any of the services to CloudWatch Logs UnicornPropertiesCatchAllRule: Type: AWS::Events::Rule Properties: - Name: contracts.catchall - Description: Catch all events published by the contracts service. + Name: properties.catchall + Description: Catchall rule used for development purposes. EventBusName: !Ref UnicornPropertiesEventBus EventPattern: account: diff --git a/unicorn_web/integration/subscriber-policies.yaml b/unicorn_web/integration/subscriber-policies.yaml index 3a32423..c7aaa03 100644 --- a/unicorn_web/integration/subscriber-policies.yaml +++ b/unicorn_web/integration/subscriber-policies.yaml @@ -20,7 +20,7 @@ Resources: Type: AWS::Events::EventBusPolicy Properties: EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" - StatementId: !Sub "${AWS::StackName}-CreateRule" + StatementId: !Sub "OnlyRulesForPropertiesServiceEvents-${Stage}" Statement: Effect: Allow Principal: diff --git a/unicorn_web/integration/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml index 16a8819..a3880c0 100644 --- a/unicorn_web/integration/subscriptions.yaml +++ b/unicorn_web/integration/subscriptions.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' -Description: Defines the cross-service subscriptions for Unicorn Web +Description: Defines the rule for the events (subscriptions) that Unicorn Properties wants to consume. Parameters: Stage: @@ -13,11 +13,12 @@ Parameters: - prod Resources: + #### UNICORN PROPERTIES EVENT SUBSCRIPTIONS PublicationEvaluationCompletedSubscriptionRule: Type: AWS::Events::Rule Properties: - Name: web.pubapprovalwf-properties.pubapprovalrequested - Description: Publication Evaluation Completed Subscription + Name: unicorn.web-PublicationEvaluationCompleted + Description: PublicationEvaluationCompleted subscription EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: @@ -31,10 +32,7 @@ Resources: RoleArn: !GetAtt UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events - # from the Unicorn Contracts event bus to the Unicorn Properties event bus. - # If the Unicorn Properties service wants receive events from other services, then add another policy - # the arn of the which includes the event bus arn where Unicorn Properties is creating the - # subscription (rule) + # from the publishing event bus, to the subscribing event bus (UnicornWebEventBusArn) UnicornPropertiesEventBusToUnicornWebEventBusRole: Type: AWS::IAM::Role Properties: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 031b2ca..40ed3ec 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -94,6 +94,7 @@ Resources: - DynamoDBReadPolicy: TableName: !Ref WebTable + # Log group for the SearchFunction SearchFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -123,6 +124,7 @@ Resources: ScalingConfig: MaximumConcurrency: 5 + # Log group for the RequestApprovalFunction RequestApprovalFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -148,7 +150,7 @@ Resources: ApprovalEvent: Type: EventBridgeRule Properties: - RuleName: web.publicationapproved-properties.pubevalcompleted + RuleName: unicorn.web-PublicationEvaluationCompleted EventBusName: !Ref UnicornWebEventBus Pattern: source: @@ -156,6 +158,7 @@ Resources: detail-type: - PublicationEvaluationCompleted + # Log group for the PublicationApprovedEventHandlerFunction PublicationApprovedEventHandlerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -277,6 +280,7 @@ Resources: Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + # DeadLetterQueue for UnicornWebIngestQueue. Contains messages that failed to be processed UnicornWebIngestDLQ: Type: AWS::SQS::Queue From 28b7345383ce0653b4e54f4a23e69446c8ad055b Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 11 Sep 2023 09:57:13 +0000 Subject: [PATCH 71/87] update dependencies (#41) * chore: update Python dependency versions * chore: lint pyproject files, fix typo in description * chore: bump code version in pyproject to match v2 tag * test: reorganised test events, refactored unit tests --- unicorn_contracts/poetry.lock | 39 ++----- unicorn_contracts/pyproject.toml | 15 +-- unicorn_properties/poetry.lock | 41 ++----- unicorn_properties/pyproject.toml | 13 +-- unicorn_properties/tests/unit/conftest.py | 16 +++ .../contract_status_changed_draft.json | 0 .../ddb_stream_events}/sfn_check_exists.json | 0 .../ddb_stream_events}/sfn_wait_approval.json | 0 .../status_approved_waiting_for_approval.json | 0 .../status_approved_with_no_workflow.json | 0 .../eventbridge}/contract_status_changed.json | 0 ...tus_changed_event_contract_1_approved.json | 0 ...status_changed_event_contract_1_draft.json | 0 ...tus_changed_event_contract_2_approved.json | 0 ...status_changed_event_contract_2_draft.json | 0 ...ion_approval_requested_event_all_good.json | 0 ...ested_event_inappropriate_description.json | 0 ..._requested_event_inappropriate_images.json | 0 ...requested_event_non_existing_contract.json | 0 ...proval_requested_event_pause_workflow.json | 0 .../publication_approval_requested_event.json | 0 ...ublication_evaluation_completed_event.json | 0 ...put_event_property_approval_requested.json | 0 ..._integrity_validator_function_success.json | 0 .../lambda/contract_status_checker.json | 0 .../wait_for_contract_approval_function.json | 0 unicorn_properties/tests/unit/helper.py | 25 +++-- .../tests/unit/lambda_context.py | 17 --- ...st_content_integrity_validator_function.py | 20 ++-- .../test_contract_exists_checker_function.py | 15 +-- ...t_contract_status_changed_event_handler.py | 12 +- .../test_properties_approval_sync_function.py | 14 +-- ...est_wait_for_contract_approval_function.py | 8 +- unicorn_web/poetry.lock | 106 ++---------------- unicorn_web/pyproject.toml | 14 +-- 35 files changed, 112 insertions(+), 243 deletions(-) rename unicorn_properties/tests/{events/dbb_stream_events => unit/events/ddb_stream_events}/contract_status_changed_draft.json (100%) rename unicorn_properties/tests/{events/dbb_stream_events => unit/events/ddb_stream_events}/sfn_check_exists.json (100%) rename unicorn_properties/tests/{events/dbb_stream_events => unit/events/ddb_stream_events}/sfn_wait_approval.json (100%) rename unicorn_properties/tests/{events/dbb_stream_events => unit/events/ddb_stream_events}/status_approved_waiting_for_approval.json (100%) rename unicorn_properties/tests/{events/dbb_stream_events => unit/events/ddb_stream_events}/status_approved_with_no_workflow.json (100%) rename unicorn_properties/tests/{events/lambda => unit/events/eventbridge}/contract_status_changed.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/contract_status_changed_event_contract_1_approved.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/contract_status_changed_event_contract_1_draft.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/contract_status_changed_event_contract_2_approved.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/contract_status_changed_event_contract_2_draft.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publicaction_approval_requested_event_all_good.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publication_approval_requested_event.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/publication_evaluation_completed_event.json (100%) rename unicorn_properties/tests/{ => unit}/events/eventbridge/put_event_property_approval_requested.json (100%) rename unicorn_properties/tests/{ => unit}/events/lambda/content_integrity_validator_function_success.json (100%) rename unicorn_properties/tests/{ => unit}/events/lambda/contract_status_checker.json (100%) rename unicorn_properties/tests/{ => unit}/events/lambda/wait_for_contract_approval_function.json (100%) delete mode 100644 unicorn_properties/tests/unit/lambda_context.py diff --git a/unicorn_contracts/poetry.lock b/unicorn_contracts/poetry.lock index 4d84f14..984c94e 100644 --- a/unicorn_contracts/poetry.lock +++ b/unicorn_contracts/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "aws-lambda-powertools" -version = "2.23.1" +version = "2.24.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, - {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, + {file = "aws_lambda_powertools-2.24.0-py3-none-any.whl", hash = "sha256:68da8646b6d2c661615e99841200dd6fa62235c99a07b0e8b04c1ca9cb1de714"}, + {file = "aws_lambda_powertools-2.24.0.tar.gz", hash = "sha256:365daef655d10346ff6c601676feef8399fed127686be3eef2b6282dd97fe88e"}, ] [package.dependencies] @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.43" +version = "1.28.44" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, - {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, + {file = "boto3-1.28.44-py3-none-any.whl", hash = "sha256:c53c92dfe22489ba31e918c2e7b59ff43e2e778bd3d3559e62351a739382bb5c"}, + {file = "boto3-1.28.44.tar.gz", hash = "sha256:eea3b07e0f28c9f92bccab972af24a3b0dd951c69d93da75227b8ecd3e18f6c4"}, ] [package.dependencies] -botocore = ">=1.31.43,<1.32.0" +botocore = ">=1.31.44,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.43" +version = "1.31.44" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, - {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, + {file = "botocore-1.31.44-py3-none-any.whl", hash = "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98"}, + {file = "botocore-1.31.44.tar.gz", hash = "sha256:84f90919fecb4a4f417fd10145c8a87ff2c4b14d6381cd34d9babf02110b3315"}, ] [package.dependencies] @@ -547,23 +547,6 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-mock" -version = "3.11.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - [[package]] name = "python-dateutil" version = "2.8.2" @@ -874,4 +857,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d98f5abd0cb97c5ab987c900089d8161e28d85067d8e3f04ba17adb06b14d871" +content-hash = "f8c69342f40c94c598c2daddf757266f294161628cb65e66646faee3acfc6be1" diff --git a/unicorn_contracts/pyproject.toml b/unicorn_contracts/pyproject.toml index d618de8..f999011 100644 --- a/unicorn_contracts/pyproject.toml +++ b/unicorn_contracts/pyproject.toml @@ -3,19 +3,17 @@ name = "contracts_service" version = "0.2.0" description = "Unicorn Properties Contact Service" authors = ["Amazon Web Services"] -packages = [ - { include = "contracts_service", from = "src" }, -] +packages = [{ include = "contracts_service", from = "src" }] [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.32" -aws-lambda-powertools = {extras = ["aws-sdk"], version = "^2.23.0"} +boto3 = "^1.28" +aws-lambda-powertools = { extras = ["aws-sdk"], version = "^2.23.0" } aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" -pytest-mock = "^3.11.1" +# pytest-mock = "^3.11.1" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" @@ -29,10 +27,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" addopts = "-ra -vv -W ignore::UserWarning" -testpaths = [ - "./tests/unit", - "./tests/integration", -] +testpaths = ["tests/unit", "tests/integration"] [tool.ruff] line-length = 150 diff --git a/unicorn_properties/poetry.lock b/unicorn_properties/poetry.lock index e6b3c98..984c94e 100644 --- a/unicorn_properties/poetry.lock +++ b/unicorn_properties/poetry.lock @@ -13,17 +13,17 @@ files = [ [[package]] name = "aws-lambda-powertools" -version = "2.23.1" +version = "2.24.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, - {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, + {file = "aws_lambda_powertools-2.24.0-py3-none-any.whl", hash = "sha256:68da8646b6d2c661615e99841200dd6fa62235c99a07b0e8b04c1ca9cb1de714"}, + {file = "aws_lambda_powertools-2.24.0.tar.gz", hash = "sha256:365daef655d10346ff6c601676feef8399fed127686be3eef2b6282dd97fe88e"}, ] [package.dependencies] -aws-xray-sdk = {version = ">=2.8.0,<3.0.0", optional = true, markers = "extra == \"tracer\" or extra == \"all\""} +boto3 = {version = ">=1.20.32,<2.0.0", optional = true, markers = "extra == \"aws-sdk\""} typing-extensions = ">=4.6.2,<5.0.0" [package.extras] @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.43" +version = "1.28.44" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, - {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, + {file = "boto3-1.28.44-py3-none-any.whl", hash = "sha256:c53c92dfe22489ba31e918c2e7b59ff43e2e778bd3d3559e62351a739382bb5c"}, + {file = "boto3-1.28.44.tar.gz", hash = "sha256:eea3b07e0f28c9f92bccab972af24a3b0dd951c69d93da75227b8ecd3e18f6c4"}, ] [package.dependencies] -botocore = ">=1.31.43,<1.32.0" +botocore = ">=1.31.44,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.43" +version = "1.31.44" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, - {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, + {file = "botocore-1.31.44-py3-none-any.whl", hash = "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98"}, + {file = "botocore-1.31.44.tar.gz", hash = "sha256:84f90919fecb4a4f417fd10145c8a87ff2c4b14d6381cd34d9babf02110b3315"}, ] [package.dependencies] @@ -547,23 +547,6 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-mock" -version = "3.11.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - [[package]] name = "python-dateutil" version = "2.8.2" @@ -874,4 +857,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "345f217fecab7cc0c0bf492f2a3648386663ca363636a462579dba5c1fa55ca6" +content-hash = "f8c69342f40c94c598c2daddf757266f294161628cb65e66646faee3acfc6be1" diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index d35e76f..93c4733 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "properties_service" -version = "0.1.0" +version = "0.2.0" description = "Unicorn Properties Property Service" authors = ["Amazon Web Services"] packages = [ @@ -10,13 +10,13 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.32" -aws-lambda-powertools = {extras = ["tracer"], version = "^2.23.0"} +boto3 = "^1.28" +aws-lambda-powertools = { extras = ["aws-sdk"], version = "^2.23.0" } aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" -pytest-mock = "^3.11.1" +# pytest-mock = "^3.11.1" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" @@ -30,10 +30,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" addopts = "-ra -vv -W ignore::UserWarning" -testpaths = [ - "tests/unit", - "tests/integration", -] +testpaths = ["tests/unit", "tests/integration"] [tool.ruff] line-length = 150 diff --git a/unicorn_properties/tests/unit/conftest.py b/unicorn_properties/tests/unit/conftest.py index 477e05d..1e09dc4 100644 --- a/unicorn_properties/tests/unit/conftest.py +++ b/unicorn_properties/tests/unit/conftest.py @@ -4,6 +4,7 @@ import os import boto3 +from aws_lambda_powertools.utilities.typing import LambdaContext import pytest from moto import mock_dynamodb, mock_events, mock_stepfunctions @@ -34,3 +35,18 @@ def eventbridge(aws_credentials): def stepfunction(aws_credentials): with mock_stepfunctions(): yield boto3.client("stepfunctions", region_name='ap-southeast-2') + + +@pytest.fixture(scope='function') +def lambda_context(): + context: LambdaContext = LambdaContext() + context._function_name="propertiesService-LambdaFunction-HJsvdah2ubi2" + context._function_version="$LATEST" + context._invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:propertiesService-LambdaFunction-HJsvdah2ubi2" + context._memory_limit_in_mb=128 + context._aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" + context._log_group_name="/aws/lambda/propertiesService-LambdaFunction-HJsvdah2ubi2" + context._log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" + # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) + # context._client_context=None + return context diff --git a/unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json b/unicorn_properties/tests/unit/events/ddb_stream_events/contract_status_changed_draft.json similarity index 100% rename from unicorn_properties/tests/events/dbb_stream_events/contract_status_changed_draft.json rename to unicorn_properties/tests/unit/events/ddb_stream_events/contract_status_changed_draft.json diff --git a/unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json b/unicorn_properties/tests/unit/events/ddb_stream_events/sfn_check_exists.json similarity index 100% rename from unicorn_properties/tests/events/dbb_stream_events/sfn_check_exists.json rename to unicorn_properties/tests/unit/events/ddb_stream_events/sfn_check_exists.json diff --git a/unicorn_properties/tests/events/dbb_stream_events/sfn_wait_approval.json b/unicorn_properties/tests/unit/events/ddb_stream_events/sfn_wait_approval.json similarity index 100% rename from unicorn_properties/tests/events/dbb_stream_events/sfn_wait_approval.json rename to unicorn_properties/tests/unit/events/ddb_stream_events/sfn_wait_approval.json diff --git a/unicorn_properties/tests/events/dbb_stream_events/status_approved_waiting_for_approval.json b/unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_waiting_for_approval.json similarity index 100% rename from unicorn_properties/tests/events/dbb_stream_events/status_approved_waiting_for_approval.json rename to unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_waiting_for_approval.json diff --git a/unicorn_properties/tests/events/dbb_stream_events/status_approved_with_no_workflow.json b/unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_with_no_workflow.json similarity index 100% rename from unicorn_properties/tests/events/dbb_stream_events/status_approved_with_no_workflow.json rename to unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_with_no_workflow.json diff --git a/unicorn_properties/tests/events/lambda/contract_status_changed.json b/unicorn_properties/tests/unit/events/eventbridge/contract_status_changed.json similarity index 100% rename from unicorn_properties/tests/events/lambda/contract_status_changed.json rename to unicorn_properties/tests/unit/events/eventbridge/contract_status_changed.json diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json b/unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_approved.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json rename to unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_approved.json diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json b/unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_draft.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json rename to unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_draft.json diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json b/unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_approved.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json rename to unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_approved.json diff --git a/unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json b/unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_draft.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json rename to unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_draft.json diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json b/unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_all_good.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_all_good.json rename to unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_all_good.json diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json b/unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json rename to unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_inappropriate_description.json diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json b/unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json rename to unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_inappropriate_images.json diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json b/unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json rename to unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_non_existing_contract.json diff --git a/unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json b/unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json rename to unicorn_properties/tests/unit/events/eventbridge/publicaction_approval_requested_event_pause_workflow.json diff --git a/unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json b/unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publication_approval_requested_event.json rename to unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event.json diff --git a/unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json b/unicorn_properties/tests/unit/events/eventbridge/publication_evaluation_completed_event.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/publication_evaluation_completed_event.json rename to unicorn_properties/tests/unit/events/eventbridge/publication_evaluation_completed_event.json diff --git a/unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json b/unicorn_properties/tests/unit/events/eventbridge/put_event_property_approval_requested.json similarity index 100% rename from unicorn_properties/tests/events/eventbridge/put_event_property_approval_requested.json rename to unicorn_properties/tests/unit/events/eventbridge/put_event_property_approval_requested.json diff --git a/unicorn_properties/tests/events/lambda/content_integrity_validator_function_success.json b/unicorn_properties/tests/unit/events/lambda/content_integrity_validator_function_success.json similarity index 100% rename from unicorn_properties/tests/events/lambda/content_integrity_validator_function_success.json rename to unicorn_properties/tests/unit/events/lambda/content_integrity_validator_function_success.json diff --git a/unicorn_properties/tests/events/lambda/contract_status_checker.json b/unicorn_properties/tests/unit/events/lambda/contract_status_checker.json similarity index 100% rename from unicorn_properties/tests/events/lambda/contract_status_checker.json rename to unicorn_properties/tests/unit/events/lambda/contract_status_checker.json diff --git a/unicorn_properties/tests/events/lambda/wait_for_contract_approval_function.json b/unicorn_properties/tests/unit/events/lambda/wait_for_contract_approval_function.json similarity index 100% rename from unicorn_properties/tests/events/lambda/wait_for_contract_approval_function.json rename to unicorn_properties/tests/unit/events/lambda/wait_for_contract_approval_function.json diff --git a/unicorn_properties/tests/unit/helper.py b/unicorn_properties/tests/unit/helper.py index ab2336b..7188309 100644 --- a/unicorn_properties/tests/unit/helper.py +++ b/unicorn_properties/tests/unit/helper.py @@ -1,28 +1,30 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import json +from pathlib import Path + TABLE_NAME = 'table1' +EVENTBUS_NAME = 'test-eventbridge' +EVENTS_DIR = Path(__file__).parent / 'events' def load_event(filename): - with open(filename) as f: - data = json.load(f) - return data + return json.load(open(EVENTS_DIR / f'{filename}.json', 'r')) def return_env_vars_dict(k={}): d = { "AWS_DEFAULT_REGION": "ap-southeast-2", "CONTRACT_STATUS_TABLE": TABLE_NAME, - "EVENT_BUS": "test-eventbridge", + "EVENT_BUS": EVENTBUS_NAME, + "SERVICE_NAMESPACE": "unicorn.properties", + "POWERTOOLS_SERVICE_NAME":"unicorn.properties", + "POWERTOOLS_TRACE_DISABLED":"true", "POWERTOOLS_LOGGER_LOG_EVENT":"true", "POWERTOOLS_LOGGER_SAMPLE_RATE":"0.1", - "POWERTOOLS_METRICS_NAMESPACE":"unicorn.contracts", - "POWERTOOLS_SERVICE_NAME":"unicorn.contracts", - "POWERTOOLS_TRACE_DISABLED":"true", - "SERVICE_NAMESPACE": "unicorn.properties", + "POWERTOOLS_METRICS_NAMESPACE":"unicorn.properties", + "LOG_LEVEL": "INFO", } d.update(k) return d @@ -89,3 +91,8 @@ def create_ddb_table_contracts_with_entry(dynamodb): } table.put_item(Item=contract) return table + + +def create_test_eventbridge_bus(eventbridge): + bus = eventbridge.create_event_bus(Name=EVENTBUS_NAME) + return bus diff --git a/unicorn_properties/tests/unit/lambda_context.py b/unicorn_properties/tests/unit/lambda_context.py deleted file mode 100644 index a260817..0000000 --- a/unicorn_properties/tests/unit/lambda_context.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -""" -Simple Lambda Context class to be passed to the lambda handler when test is invoked -""" - -class LambdaContext: - aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" - log_group_name="/aws/lambda/propertiesService-ContractStatusChanged-IWaQgsTEtLtX" - log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" - function_name="propertiesService-ContractStatusChanged-IWaQgsTEtLtX" - memory_limit_in_mb=128 - function_version="$LATEST" - invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:propertiesService-ContractStatusChanged-IWaQgsTEtLtX" - client_context=None - #identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) diff --git a/unicorn_properties/tests/unit/test_content_integrity_validator_function.py b/unicorn_properties/tests/unit/test_content_integrity_validator_function.py index dc8cc00..b87f6e7 100644 --- a/unicorn_properties/tests/unit/test_content_integrity_validator_function.py +++ b/unicorn_properties/tests/unit/test_content_integrity_validator_function.py @@ -1,18 +1,16 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os import pytest from unittest import mock -from .lambda_context import LambdaContext from .helper import load_event, return_env_vars_dict @pytest.fixture def stepfunctions_event(): - return load_event('tests/events/lambda/content_integrity_validator_function_success.json') + return load_event('lambda/content_integrity_validator_function_success') @pytest.fixture @@ -49,9 +47,9 @@ def invalid_content_sentiment(stepfunctions_event): @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_valid_image_and_valid_sentiment(stepfunctions_event): +def test_valid_image_and_valid_sentiment(stepfunctions_event, lambda_context): from properties_service import content_integrity_validator_function - ret = content_integrity_validator_function.lambda_handler(stepfunctions_event, LambdaContext()) + ret = content_integrity_validator_function.lambda_handler(stepfunctions_event, lambda_context) assert ret['validation_result'] == "PASS" assert ret['imageModerations'] == stepfunctions_event['imageModerations'] @@ -59,11 +57,11 @@ def test_valid_image_and_valid_sentiment(stepfunctions_event): @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_valid_image_and_invalid_sentiment(invalid_content_sentiment): +def test_valid_image_and_invalid_sentiment(invalid_content_sentiment, lambda_context): event = invalid_content_sentiment from properties_service import content_integrity_validator_function - ret = content_integrity_validator_function.lambda_handler(event, LambdaContext()) + ret = content_integrity_validator_function.lambda_handler(event, lambda_context) assert ret['validation_result'] == "FAIL" assert ret['imageModerations'] == event['imageModerations'] @@ -71,11 +69,11 @@ def test_valid_image_and_invalid_sentiment(invalid_content_sentiment): @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_invalid_image_and_valid_sentiment(invalid_image_moderation): +def test_invalid_image_and_valid_sentiment(invalid_image_moderation, lambda_context): event = invalid_image_moderation from properties_service import content_integrity_validator_function - ret = content_integrity_validator_function.lambda_handler(event, LambdaContext()) + ret = content_integrity_validator_function.lambda_handler(event, lambda_context) assert ret['validation_result'] == "FAIL" assert ret['imageModerations'] == event['imageModerations'] @@ -83,11 +81,11 @@ def test_invalid_image_and_valid_sentiment(invalid_image_moderation): @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_invalid_image_and_invalid_sentiment(invalid_image_moderation, invalid_content_sentiment): +def test_invalid_image_and_invalid_sentiment(invalid_image_moderation, invalid_content_sentiment, lambda_context): event = {**invalid_image_moderation, **invalid_content_sentiment} from properties_service import content_integrity_validator_function - ret = content_integrity_validator_function.lambda_handler(event, LambdaContext()) + ret = content_integrity_validator_function.lambda_handler(event, lambda_context) assert ret['validation_result'] == "FAIL" assert ret['imageModerations'] == event['imageModerations'] diff --git a/unicorn_properties/tests/unit/test_contract_exists_checker_function.py b/unicorn_properties/tests/unit/test_contract_exists_checker_function.py index 277c5f0..ec91718 100644 --- a/unicorn_properties/tests/unit/test_contract_exists_checker_function.py +++ b/unicorn_properties/tests/unit/test_contract_exists_checker_function.py @@ -1,34 +1,31 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os from importlib import reload import pytest from unittest import mock -from .lambda_context import LambdaContext from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_existing_contract_exists_checker_function(dynamodb, mocker): - stepfunctions_event = load_event('tests/events/lambda/contract_status_checker.json') - +def test_existing_contract_exists_checker_function(dynamodb, lambda_context): + stepfunctions_event = load_event('lambda/contract_status_checker') from properties_service import contract_exists_checker_function reload(contract_exists_checker_function) create_ddb_table_contracts_with_entry(dynamodb) - ret = contract_exists_checker_function.lambda_handler(stepfunctions_event, LambdaContext()) + ret = contract_exists_checker_function.lambda_handler(stepfunctions_event, lambda_context) assert ret['property_id'] == stepfunctions_event['Input']['property_id'] assert ret['address']['country'] == stepfunctions_event['Input']['country'] @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_contract_exists_checker_function(dynamodb, mocker): - stepfunctions_event = load_event('tests/events/lambda/contract_status_checker.json') +def test_missing_contract_exists_checker_function(dynamodb, lambda_context): + stepfunctions_event = load_event('lambda/contract_status_checker') stepfunctions_event['Input']['property_id'] = 'NOT/a/valid/CONTRACT' from properties_service import contract_exists_checker_function @@ -38,6 +35,6 @@ def test_missing_contract_exists_checker_function(dynamodb, mocker): create_ddb_table_contracts_with_entry(dynamodb) with pytest.raises(ContractStatusNotFoundException) as errinfo: - contract_exists_checker_function.lambda_handler(stepfunctions_event, LambdaContext()) + contract_exists_checker_function.lambda_handler(stepfunctions_event, lambda_context) assert errinfo.value.message == 'No contract found for specified Property ID' diff --git a/unicorn_properties/tests/unit/test_contract_status_changed_event_handler.py b/unicorn_properties/tests/unit/test_contract_status_changed_event_handler.py index cb13c52..12e5d59 100644 --- a/unicorn_properties/tests/unit/test_contract_status_changed_event_handler.py +++ b/unicorn_properties/tests/unit/test_contract_status_changed_event_handler.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os from importlib import reload @@ -8,13 +7,12 @@ from unittest import mock from botocore.exceptions import ClientError -from .lambda_context import LambdaContext from .helper import load_event, return_env_vars_dict, create_ddb_table_properties @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_contract_status_changed_event_handler(dynamodb, mocker): - eventbridge_event = load_event('tests/events/lambda/contract_status_changed.json') +def test_contract_status_changed_event_handler(dynamodb, lambda_context): + eventbridge_event = load_event('eventbridge/contract_status_changed') from properties_service import contract_status_changed_event_handler # Reload is required to prevent function setup reuse from another test @@ -22,13 +20,13 @@ def test_contract_status_changed_event_handler(dynamodb, mocker): create_ddb_table_properties(dynamodb) - ret = contract_status_changed_event_handler.lambda_handler(eventbridge_event, LambdaContext()) + ret = contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) assert ret["statusCode"] == 200 @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_property_id(dynamodb, mocker): +def test_missing_property_id(dynamodb, lambda_context): eventbridge_event = {'detail': {}} from properties_service import contract_status_changed_event_handler @@ -38,6 +36,6 @@ def test_missing_property_id(dynamodb, mocker): create_ddb_table_properties(dynamodb) with pytest.raises(ClientError) as e: - contract_status_changed_event_handler.lambda_handler(eventbridge_event, LambdaContext()) + contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) assert 'ValidationException' in str(e.value) diff --git a/unicorn_properties/tests/unit/test_properties_approval_sync_function.py b/unicorn_properties/tests/unit/test_properties_approval_sync_function.py index 5612809..4af568f 100644 --- a/unicorn_properties/tests/unit/test_properties_approval_sync_function.py +++ b/unicorn_properties/tests/unit/test_properties_approval_sync_function.py @@ -1,37 +1,35 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os from importlib import reload from unittest import mock -from .lambda_context import LambdaContext from .helper import load_event, return_env_vars_dict @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_handle_status_changed_draft(stepfunction, mocker): - ddbstream_event = load_event('tests/events/dbb_stream_events/contract_status_changed_draft.json') +def test_handle_status_changed_draft(stepfunction, lambda_context): + ddbstream_event = load_event('ddb_stream_events/contract_status_changed_draft') from properties_service import properties_approval_sync_function reload(properties_approval_sync_function) - ret = properties_approval_sync_function.lambda_handler(ddbstream_event, LambdaContext()) + ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) assert ret is None # NOTE: This test cannot be implemented at this time because `moto`` does not yet support mocking `stepfunctions.send_task_success` @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_handle_status_changed_approved(caplog, stepfunction, mocker): +def test_handle_status_changed_approved(caplog, stepfunction, lambda_context): pass - # ddbstream_event = load_event('tests/events/dbb_stream_events/status_approved_waiting_for_approval.json') + # ddbstream_event = load_event('ddb_stream_events/status_approved_waiting_for_approval') # from properties_service import properties_approval_sync_function # reload(properties_approval_sync_function) - # ret = properties_approval_sync_function.lambda_handler(ddbstream_event, LambdaContext()) + # ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) # assert ret is None # assert 'Contract status for property is APPROVED' in caplog.text diff --git a/unicorn_properties/tests/unit/test_wait_for_contract_approval_function.py b/unicorn_properties/tests/unit/test_wait_for_contract_approval_function.py index c5865d1..1e05d15 100644 --- a/unicorn_properties/tests/unit/test_wait_for_contract_approval_function.py +++ b/unicorn_properties/tests/unit/test_wait_for_contract_approval_function.py @@ -1,18 +1,16 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 - import os from importlib import reload from unittest import mock -from .lambda_context import LambdaContext from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_handle_wait_for_contract_approval_function(dynamodb, mocker): - stepfunctions_event = load_event('tests/events/lambda/wait_for_contract_approval_function.json') +def test_handle_wait_for_contract_approval_function(dynamodb, lambda_context): + stepfunctions_event = load_event('lambda/wait_for_contract_approval_function') from properties_service import wait_for_contract_approval_function reload(wait_for_contract_approval_function) @@ -22,7 +20,7 @@ def test_handle_wait_for_contract_approval_function(dynamodb, mocker): ddbitem_before = dynamodb.Table('table1').get_item(Key={'property_id': stepfunctions_event['Input']['property_id']}) assert 'sfn_wait_approved_task_token' not in ddbitem_before['Item'] - ret = wait_for_contract_approval_function.lambda_handler(stepfunctions_event, LambdaContext()) + ret = wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context) ddbitem_after = dynamodb.Table('table1').get_item(Key={'property_id': stepfunctions_event['Input']['property_id']}) assert ret['property_id'] == stepfunctions_event['Input']['property_id'] diff --git a/unicorn_web/poetry.lock b/unicorn_web/poetry.lock index f0b5dc0..0713cf8 100644 --- a/unicorn_web/poetry.lock +++ b/unicorn_web/poetry.lock @@ -13,17 +13,17 @@ files = [ [[package]] name = "aws-lambda-powertools" -version = "2.23.1" +version = "2.24.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false python-versions = ">=3.7.4,<4.0.0" files = [ - {file = "aws_lambda_powertools-2.23.1-py3-none-any.whl", hash = "sha256:daa42b1060bc42bf473c984257345bef17e5bded18ec1d6680ce97ca25c1dfbb"}, - {file = "aws_lambda_powertools-2.23.1.tar.gz", hash = "sha256:1dc08eddd04520cb400085c13db162cf84020d4803192d88c0d2742d77ce3758"}, + {file = "aws_lambda_powertools-2.24.0-py3-none-any.whl", hash = "sha256:68da8646b6d2c661615e99841200dd6fa62235c99a07b0e8b04c1ca9cb1de714"}, + {file = "aws_lambda_powertools-2.24.0.tar.gz", hash = "sha256:365daef655d10346ff6c601676feef8399fed127686be3eef2b6282dd97fe88e"}, ] [package.dependencies] -aws-xray-sdk = {version = ">=2.8.0,<3.0.0", optional = true, markers = "extra == \"tracer\" or extra == \"all\""} +boto3 = {version = ">=1.20.32,<2.0.0", optional = true, markers = "extra == \"aws-sdk\""} typing-extensions = ">=4.6.2,<5.0.0" [package.extras] @@ -51,17 +51,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.28.43" +version = "1.28.44" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.28.43-py3-none-any.whl", hash = "sha256:4cd3e96900fb50bddc9f48007176c80d15396d08c5248b25a41220f3570e014f"}, - {file = "boto3-1.28.43.tar.gz", hash = "sha256:c0211a3e830432851c73fa1e136b14dbb6d02b5c9a5e1272c557e63538620b88"}, + {file = "boto3-1.28.44-py3-none-any.whl", hash = "sha256:c53c92dfe22489ba31e918c2e7b59ff43e2e778bd3d3559e62351a739382bb5c"}, + {file = "boto3-1.28.44.tar.gz", hash = "sha256:eea3b07e0f28c9f92bccab972af24a3b0dd951c69d93da75227b8ecd3e18f6c4"}, ] [package.dependencies] -botocore = ">=1.31.43,<1.32.0" +botocore = ">=1.31.44,<1.32.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -70,13 +70,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.43" +version = "1.31.44" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.43-py3-none-any.whl", hash = "sha256:d8b0c41c8c75d82f15fee57f7d54a852a99810faacbeb9d6f3f022558a2c330e"}, - {file = "botocore-1.31.43.tar.gz", hash = "sha256:b4a3a1fcf75011351e2b0d3eb991f51f8d44a375d3e065f907dac67db232fc97"}, + {file = "botocore-1.31.44-py3-none-any.whl", hash = "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98"}, + {file = "botocore-1.31.44.tar.gz", hash = "sha256:84f90919fecb4a4f417fd10145c8a87ff2c4b14d6381cd34d9babf02110b3315"}, ] [package.dependencies] @@ -269,70 +269,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "coverage" -version = "7.3.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, - {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, - {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, - {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, - {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, - {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, - {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, - {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, - {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, - {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, - {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, - {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, - {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, - {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, -] - -[package.extras] -toml = ["tomli"] - [[package]] name = "crhelper" version = "2.0.11" @@ -622,24 +558,6 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - [[package]] name = "python-dateutil" version = "2.8.2" @@ -950,4 +868,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "9e185582d06d620a83ded07aa755c7c7c8b268abff3ac879567f50aff7b0ec1f" +content-hash = "7906d3815c35a7d13ad02fcc3668a57afecb4bdf555dfa9ae78552391793ee3a" diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index 07003d2..3b84396 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "unicorn_web" -version = "0.1.0" -description = "Unicorn Properties Web" +version = "0.2.0" +description = "Unicorn Properties Web Service" authors = ["Amazon Web Services"] packages = [ { include = "approvals_service", from = "src" }, @@ -10,18 +10,18 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -boto3 = "^1.28.32" -aws-lambda-powertools = {extras = ["tracer"], version = "^2.23.0"} +boto3 = "^1.28" +aws-lambda-powertools = { extras = ["aws-sdk"], version = "^2.23.0" } aws-xray-sdk = "^2.12.0" requests = "2.31.0" crhelper = "^2.0.11" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" -pytest-cov = "^4.1.0" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" +pyyaml = "^6.0.1" arnparse = "^0.0.2" [build-system] @@ -31,9 +31,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" addopts = "-ra -vv -W ignore::UserWarning" -testpaths = [ - "./tests/unit", -] +testpaths = ["tests/unit", "tests/integration"] [tool.ruff] line-length = 150 From a261399582e85feaf50ec58ffb8bbd2151b9fe68 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 11 Sep 2023 09:57:55 +0000 Subject: [PATCH 72/87] nested stacks (#42) * refactor: Moved each services ancillary stacks to be nested inside template.yaml, to simplify CI/CD deployment * chore: fix template.yaml linting * test: add outputs that help with integration tests * chore: add description and comments to template.yaml sections * fix: typo in SAM policy template usage * test: reorganised test events, refactored unit tests --- .../integration/event-schemas.yaml | 183 ++++++------- .../integration/subscriber-policies.yaml | 16 +- unicorn_contracts/samconfig.yaml | 35 +-- unicorn_contracts/template.yaml | 36 ++- .../integration/event-schemas.yaml | 157 +++++------ .../integration/subscriber-policies.yaml | 16 +- .../integration/subscriptions.yaml | 31 ++- unicorn_properties/samconfig.yaml | 49 +--- unicorn_properties/template.yaml | 65 +++-- unicorn_web/integration/event-schemas.yaml | 251 +++++++++--------- .../integration/subscriber-policies.yaml | 16 +- unicorn_web/integration/subscriptions.yaml | 17 +- unicorn_web/samconfig.yaml | 52 +--- unicorn_web/template.yaml | 52 +++- 14 files changed, 494 insertions(+), 482 deletions(-) diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index 05b3399..9ac7084 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -AWSTemplateFormatVersion: '2010-09-09' -Description: | +AWSTemplateFormatVersion: "2010-09-09" +Description: > Defines the event bus policies that determine who can create rules on the event bus to subscribe to events published by Unicorn Contracts Service. @@ -19,12 +19,14 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Contracts' - RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + RegistryName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy - Properties: - RegistryName: !GetAtt EventRegistry.RegistryName + Properties: + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName Policy: Version: '2012-10-17' Statement: @@ -32,7 +34,7 @@ Resources: Effect: Allow Principal: AWS: - - !Ref AWS::AccountId + - Ref: AWS::AccountId Action: - schemas:DescribeCodeBinding - schemas:DescribeRegistry @@ -42,99 +44,102 @@ Resources: - schemas:ListSchemaVersions - schemas:SearchSchemas Resource: - - !GetAtt EventRegistry.RegistryArn - - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + - Fn::GetAtt: EventRegistry.RegistryArn + - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" ContractApprovedEventSchema: Type: AWS::EventSchemas::Schema Properties: Type: 'OpenApi3' - RegistryName: !GetAtt EventRegistry.RegistryName - SchemaName: !Sub '${EventRegistry.RegistryName}@ContractStatusChanged' + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName + SchemaName: + Fn::Sub: "${EventRegistry.RegistryName}@ContractStatusChanged" Description: 'The schema for a request to publish a property' - Content: !Sub | - { - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "ContractStatusChanged" - }, - "paths": {}, - "components": { - "schemas": { - "AWSEvent": { - "type": "object", - "required": [ - "detail-type", - "resources", - "detail", - "id", - "source", - "time", - "region", - "version", - "account" - ], - "x-amazon-events-detail-type": "ContractStatusChanged", - "x-amazon-events-source": "${EventRegistry.RegistryName}", - "properties": { - "detail": { - "$ref": "#/components/schemas/ContractStatusChanged" - }, - "account": { - "type": "string" - }, - "detail-type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "region": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "object" + Content: + Fn::Sub: | + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "ContractStatusChanged" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "ContractStatusChanged", + "x-amazon-events-source": "${EventRegistry.RegistryName}", + "properties": { + "detail": { + "$ref": "#/components/schemas/ContractStatusChanged" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" } - }, - "source": { - "type": "string" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" } - } - }, - "ContractStatusChanged": { - "type": "object", - "required": [ - "contract_last_modified_on", - "contract_id", - "contract_status", - "property_id" - ], - "properties": { - "contract_id": { - "type": "string" - }, - "contract_last_modified_on": { - "type": "string", - "format": "date-time" - }, - "contract_status": { - "type": "string" - }, - "property_id": { - "type": "string" + }, + "ContractStatusChanged": { + "type": "object", + "required": [ + "contract_last_modified_on", + "contract_id", + "contract_status", + "property_id" + ], + "properties": { + "contract_id": { + "type": "string" + }, + "contract_last_modified_on": { + "type": "string", + "format": "date-time" + }, + "contract_status": { + "type": "string" + }, + "property_id": { + "type": "string" + } } } } } } - } diff --git a/unicorn_contracts/integration/subscriber-policies.yaml b/unicorn_contracts/integration/subscriber-policies.yaml index 929d627..4f5e210 100644 --- a/unicorn_contracts/integration/subscriber-policies.yaml +++ b/unicorn_contracts/integration/subscriber-policies.yaml @@ -20,12 +20,15 @@ Resources: CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" - StatementId: !Sub "OnlyRulesForContractServiceEvents-${Stage}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" + StatementId: + Fn::Sub: "OnlyRulesForContractServiceEvents-${Stage}" Statement: Effect: Allow Principal: - AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + AWS: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" Action: - events:PutRule - events:DeleteRule @@ -35,14 +38,15 @@ Resources: - events:PutTargets - events:RemoveTargets Resource: - - !Sub + - Fn::Sub: - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" + - eventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_contracts/samconfig.yaml b/unicorn_contracts/samconfig.yaml index 8c67cf3..e4a47b9 100644 --- a/unicorn_contracts/samconfig.yaml +++ b/unicorn_contracts/samconfig.yaml @@ -16,7 +16,9 @@ default: disable_rollback: true confirm_changeset: false fail_on_empty_changeset: false - capabilities: CAPABILITY_IAM + capabilities: + - CAPABILITY_IAM + - CAPABILITY_AUTO_EXPAND parameter_overrides: - "Stage=local" validate: @@ -31,34 +33,3 @@ default: local_start_lambda: parameters: warm_containers: EAGER - -schemas: - global: - parameters: - stack_name: uni-prop-local-contracts-schemas - s3_prefix: uni-prop-local-contracts-schemas - resolve_s3: true - deploy: - parameters: - template_file: integration/event-schemas.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - parameter_overrides: - - "Stage=local" - -subscriber-policies: - global: - parameters: - stack_name: uni-prop-local-contracts-subscriber-policies - s3_prefix: uni-prop-local-contracts-subscriber-policies - resolve_s3: true - deploy: - parameters: - template_file: ./integration/subscriber-policies.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - capabilities: CAPABILITY_IAM - parameter_overrides: - - "Stage=local" diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 1cb7085..a264ee4 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -11,10 +11,11 @@ Metadata: cfn-lint: config: ignore_checks: - - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ - - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy - - WS2001 # Rule disabled because check does not support !ToJsonString transform - - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - WS2001 # Rule disabled because check does not support !ToJsonString transform + - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure + - W3002 Parameters: Stage: @@ -83,7 +84,6 @@ Resources: Name: !Sub /uni-prop/${Stage}/UnicornContractsEventBusArn Value: !GetAtt UnicornContractsEventBus.Arn - #### LAMBDA FUNCTIONS # Processes customer API requests from SQS queue UnicornContractsIngestQueue ContractEventHandlerFunction: @@ -195,7 +195,6 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs - #### INGEST QUEUES # Queue API Gateway requests to be processed by ContractEventHandlerFunction UnicornContractsIngestQueue: @@ -217,7 +216,7 @@ Resources: Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" - + #### DEAD LETTER QUEUES # DeadLetterQueue for UnicornContractsIngestQueue. Contains messages that failed to be processed UnicornContractsIngestDLQ: @@ -260,7 +259,6 @@ Resources: - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" - #### EVENT BUS # Event bus for Unicorn Contract Service used to publish and consume events UnicornContractsEventBus: @@ -349,7 +347,6 @@ Resources: ] } - #### EVENT BRIDGE PIPES # Pipe changed on DynamoDB Table to UnicornContractsEventBus DdbStreamToEventPipe: @@ -413,6 +410,22 @@ Resources: - events:PutEvents Resource: !GetAtt UnicornContractsEventBus.Arn + #### CLOUDFORMATION NESTED STACKS + EventSchemasStack: + Type: AWS::Serverless::Application + Properties: + Location: "integration/event-schemas.yaml" + Parameters: + Stage: !Ref Stage + SubscriberPoliciesStack: + Type: AWS::Serverless::Application + DependsOn: + - UnicornContractsEventBusNameParam + Properties: + Location: "integration/subscriber-policies.yaml" + Parameters: + Stage: !Ref Stage + Outputs: #### API GATEWAY OUTPUTS BaseUrl: @@ -440,6 +453,11 @@ Outputs: Description: ContractEventHandler function ARN Value: !GetAtt ContractEventHandlerFunction.Arn + #### EVENT BRIDGE OUTPUTS + UnicornContractsEventBusName: + Value: !GetAtt UnicornContractsEventBus.Name + #### CLOUDWATCH LOGS OUTPUTS UnicornContractsCatchAllLogGroupArn: + Description: Log all events on the service's EventBridge Bus Value: !GetAtt UnicornContractsCatchAllLogGroup.Arn diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_properties/integration/event-schemas.yaml index c12cae8..47a9789 100644 --- a/unicorn_properties/integration/event-schemas.yaml +++ b/unicorn_properties/integration/event-schemas.yaml @@ -17,12 +17,14 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Properties' - RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + RegistryName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy Properties: - RegistryName: !GetAtt EventRegistry.RegistryName + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName Policy: Version: '2012-10-17' Statement: @@ -30,7 +32,7 @@ Resources: Effect: Allow Principal: AWS: - - !Ref AWS::AccountId + - Ref: AWS::AccountId Action: - schemas:DescribeCodeBinding - schemas:DescribeRegistry @@ -40,90 +42,93 @@ Resources: - schemas:ListSchemaVersions - schemas:SearchSchemas Resource: - - !GetAtt EventRegistry.RegistryArn - - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + - Fn::GetAtt: EventRegistry.RegistryArn + - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" PublicationEvaluationCompleted: Type: AWS::EventSchemas::Schema Properties: Type: 'OpenApi3' - RegistryName: !GetAtt EventRegistry.RegistryName - SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName + SchemaName: + Fn::Sub: '${EventRegistry.RegistryName}@PublicationEvaluationCompleted' Description: 'The schema for when a property evaluation is completed' - Content: !Sub | - { - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "PublicationEvaluationCompleted" - }, - "paths": {}, - "components": { - "schemas": { - "AWSEvent": { - "type": "object", - "required": [ - "detail-type", - "resources", - "detail", - "id", - "source", - "time", - "region", - "version", - "account" - ], - "x-amazon-events-detail-type": "PublicationEvaluationCompleted", - "x-amazon-events-source": "${EventRegistry.RegistryName}", - "properties": { - "detail": { - "$ref": "#/components/schemas/PublicationEvaluationCompleted" - }, - "account": { - "type": "string" - }, - "detail-type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "region": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { + Content: + Fn::Sub: | + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationEvaluationCompleted" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationEvaluationCompleted", + "x-amazon-events-source": "${EventRegistry.RegistryName}", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationEvaluationCompleted" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { "type": "string" } - }, - "source": { - "type": "string" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" } - } - }, - "PublicationEvaluationCompleted": { - "type": "object", - "required": [ - "property_id", - "evaluation_result" - ], - "properties": { - "property_id": { - "type": "string" - }, - "evaluation_result": { - "type": "string" + }, + "PublicationEvaluationCompleted": { + "type": "object", + "required": [ + "property_id", + "evaluation_result" + ], + "properties": { + "property_id": { + "type": "string" + }, + "evaluation_result": { + "type": "string" + } } } } } } - } diff --git a/unicorn_properties/integration/subscriber-policies.yaml b/unicorn_properties/integration/subscriber-policies.yaml index 32d3441..2bbe786 100644 --- a/unicorn_properties/integration/subscriber-policies.yaml +++ b/unicorn_properties/integration/subscriber-policies.yaml @@ -19,12 +19,15 @@ Resources: CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" - StatementId: !Sub "OnlyRulesForPropertiesServiceEvents-${Stage}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" + StatementId: + Fn::Sub: "OnlyRulesForPropertiesServiceEvents-${Stage}" Statement: Effect: Allow Principal: - AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + AWS: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" Action: - events:PutRule - events:DeleteRule @@ -34,14 +37,15 @@ Resources: - events:PutTargets - events:RemoveTargets Resource: - - !Sub + - Fn::Sub: - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" + - eventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_properties/integration/subscriptions.yaml b/unicorn_properties/integration/subscriptions.yaml index 0536820..871e0fa 100644 --- a/unicorn_properties/integration/subscriptions.yaml +++ b/unicorn_properties/integration/subscriptions.yaml @@ -20,17 +20,20 @@ Resources: Properties: Name: unicorn.properties-ContractStatusChanged Description: Contract Status Changed subscription - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" detail-type: - ContractStatusChanged State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" - RoleArn: !GetAtt UnicornPropertiesSubscriptionRole.Arn + Arn: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + RoleArn: + Fn::GetAtt: UnicornPropertiesSubscriptionRole.Arn #### UNICORN WEB EVENT SUBSCRIPTIONS PublicationApprovalRequestedSubscriptionRule: @@ -38,17 +41,20 @@ Resources: Properties: Name: unicorn.properties-PublicationApprovalRequested Description: Publication evaluation completed subscription - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" detail-type: - PublicationApprovalRequested State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" - RoleArn: !GetAtt UnicornPropertiesSubscriptionRole.Arn + Arn: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + RoleArn: + Fn::GetAtt: UnicornPropertiesSubscriptionRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events @@ -68,13 +74,16 @@ Resources: Statement: - Effect: Allow Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + Resource: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" Outputs: ContractStatusChangedSubscription: Description: Rule ARN for Contract service event subscription - Value: !GetAtt ContractStatusChangedSubscriptionRule.Arn + Value: + Fn::GetAtt: ContractStatusChangedSubscriptionRule.Arn PublicationApprovalRequestedSubscription: Description: Rule ARN for Web service event subscription - Value: !GetAtt PublicationApprovalRequestedSubscriptionRule.Arn + Value: + Fn::GetAtt: PublicationApprovalRequestedSubscriptionRule.Arn diff --git a/unicorn_properties/samconfig.yaml b/unicorn_properties/samconfig.yaml index 94eac2b..3829e33 100644 --- a/unicorn_properties/samconfig.yaml +++ b/unicorn_properties/samconfig.yaml @@ -16,7 +16,9 @@ default: disable_rollback: true confirm_changeset: false fail_on_empty_changeset: false - capabilities: CAPABILITY_IAM + capabilities: + - CAPABILITY_IAM + - CAPABILITY_AUTO_EXPAND parameter_overrides: - "Stage=local" validate: @@ -31,48 +33,3 @@ default: local_start_lambda: parameters: warm_containers: EAGER - -schemas: - deploy: - parameters: - stack_name: uni-prop-local-properties-schemas - s3_prefix: uni-prop-local-properties-schemas - template_file: integration/event-schemas.yaml - resolve_s3: true - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - parameter_overrides: - - "Stage=local" - -subscriber-policies: - global: - parameters: - stack_name: uni-prop-local-properties-subscriber-policies - s3_prefix: uni-prop-local-properties-subscriber-policies - resolve_s3: true - deploy: - parameters: - template_file: ./integration/subscriber-policies.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - capabilities: CAPABILITY_IAM - parameter_overrides: - - "Stage=local" - -subscriptions: - global: - parameters: - stack_name: uni-prop-local-properties-subscriptions - s3_prefix: uni-prop-local-properties-subscriptions - resolve_s3: true - deploy: - parameters: - template_file: ./integration/subscriptions.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - capabilities: CAPABILITY_IAM - parameter_overrides: - - "Stage=local" diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 2e8100f..2722823 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -11,8 +11,9 @@ Metadata: cfn-lint: config: ignore_checks: - - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ - - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - E0001 # Rule disabled because cfn-lint cannot parse SAM Policy templates without arguments (ComprehendBasicAccessPolicy, RekognitionDetectOnlyPolicy) Parameters: Stage: @@ -36,9 +37,7 @@ Mappings: Value: "AWS Serverless Developer Experience" Conditions: - IsProd: !Equals - - !Ref Stage - - Prod + IsProd: !Equals [!Ref Stage, Prod] Globals: Function: @@ -61,7 +60,7 @@ Globals: LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage - project: !FindInMap [ Constants, ProjectName, Value ] + project: !FindInMap [Constants, ProjectName, Value] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" Resources: @@ -73,7 +72,7 @@ Resources: Type: String Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBus Value: !GetAtt UnicornPropertiesEventBus.Name - + UnicornPropertiesEventBusArnParam: Type: AWS::SSM::Parameter Properties: @@ -81,7 +80,6 @@ Resources: Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBusArn Value: !GetAtt UnicornPropertiesEventBus.Arn - #### LAMBDA FUNCTIONS # Listens to ContractStatusChanged events from EventBridge ContractStatusChangedHandlerFunction: @@ -182,7 +180,7 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref ContractStatusTable - # Log group for the ContractExistsCheckerFunction + # Log group for the ContractExistsCheckerFunction ContractExistsCheckerFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -236,8 +234,8 @@ Resources: Enabled: true Policies: - AWSXRayDaemonWriteAccess - - ComprehendBasicAccessPolicy - - RekognitionDetectOnlyPolicy + - ComprehendBasicAccessPolicy: {} + - RekognitionDetectOnlyPolicy: {} - LambdaInvokePolicy: FunctionName: !Ref WaitForContractApprovalFunction - LambdaInvokePolicy: @@ -312,7 +310,7 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: !FindInMap [ Constants, ProjectName, Value ] + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage @@ -328,7 +326,7 @@ Resources: MessageRetentionPeriod: 1209600 # Maximum value, 1,209,600 (14days) Tags: - Key: project - Value: !FindInMap [ Constants, ProjectName, Value ] + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage @@ -351,13 +349,12 @@ Resources: BillingMode: PAY_PER_REQUEST Tags: - Key: project - Value: !FindInMap [ Constants, ProjectName, Value ] + Value: !FindInMap [Constants, ProjectName, Value] - Key: namespace Value: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" - Key: stage Value: !Ref Stage - #### EVENT BUS # Event bus for Unicorn Properties Service, used to publish and consume events UnicornPropertiesEventBus: @@ -443,6 +440,30 @@ Resources: ] } + #### CLOUDFORMATION NESTED STACKS + EventSchemasStack: + Type: AWS::Serverless::Application + Properties: + Location: "integration/event-schemas.yaml" + Parameters: + Stage: !Ref Stage + SubscriberPoliciesStack: + Type: AWS::Serverless::Application + DependsOn: + - UnicornPropertiesEventBusNameParam + Properties: + Location: "integration/subscriber-policies.yaml" + Parameters: + Stage: !Ref Stage + SubscriptionsStack: + Type: AWS::Serverless::Application + DependsOn: + - UnicornPropertiesEventBusArnParam + Properties: + Location: "integration/subscriptions.yaml" + Parameters: + Stage: !Ref Stage + Outputs: #### DYNAMODB OUTPUTS ContractStatusTableName: @@ -475,6 +496,20 @@ Outputs: WaitForContractApprovalFunctionArn: Value: !GetAtt WaitForContractApprovalFunction.Arn + #### STEPFUNCTIONS OUTPUTS + ApprovalStateMachineName: + Value: !GetAtt ApprovalStateMachine.Name + ApprovalStateMachineArn: + Value: !Ref ApprovalStateMachine + + #### EVENT BRIDGE OUTPUTS + UnicornPropertiesEventBusName: + Value: !GetAtt UnicornPropertiesEventBus.Name + #### CLOUDWATCH LOGS OUTPUTS UnicornPropertiesCatchAllLogGroupArn: + Description: Log all events on the service's EventBridge Bus Value: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn + + ApprovalStateMachineLogGroupName: + Value: !Ref ApprovalStateMachineLogGroup diff --git a/unicorn_web/integration/event-schemas.yaml b/unicorn_web/integration/event-schemas.yaml index 91a298b..cad6453 100644 --- a/unicorn_web/integration/event-schemas.yaml +++ b/unicorn_web/integration/event-schemas.yaml @@ -17,12 +17,14 @@ Resources: Type: AWS::EventSchemas::Registry Properties: Description: 'Event schemas for Unicorn Web' - RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + RegistryName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" EventRegistryPolicy: Type: AWS::EventSchemas::RegistryPolicy Properties: - RegistryName: !GetAtt EventRegistry.RegistryName + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName Policy: Version: '2012-10-17' Statement: @@ -30,7 +32,7 @@ Resources: Effect: Allow Principal: AWS: - - !Ref AWS::AccountId + - Ref: AWS::AccountId Action: - schemas:DescribeCodeBinding - schemas:DescribeRegistry @@ -40,140 +42,143 @@ Resources: - schemas:ListSchemaVersions - schemas:SearchSchemas Resource: - - !GetAtt EventRegistry.RegistryArn - - !Sub "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" + - Fn::GetAtt: EventRegistry.RegistryArn + - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" PublicationApprovalRequested: Type: AWS::EventSchemas::Schema Properties: Type: 'OpenApi3' - RegistryName: !GetAtt EventRegistry.RegistryName - SchemaName: !Sub '${EventRegistry.RegistryName}@PublicationApprovalRequested' + RegistryName: + Fn::GetAtt: EventRegistry.RegistryName + SchemaName: + Fn::Sub: '${EventRegistry.RegistryName}@PublicationApprovalRequested' Description: 'The schema for a request to publish a property' - Content: !Sub | - { - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "PublicationApprovalRequested" - }, - "paths": {}, - "components": { - "schemas": { - "AWSEvent": { - "type": "object", - "required": [ - "detail-type", - "resources", - "detail", - "id", - "source", - "time", - "region", - "version", - "account" - ], - "x-amazon-events-detail-type": "PublicationApprovalRequested", - "x-amazon-events-source": "${EventRegistry.RegistryName}", - "properties": { - "detail": { - "$ref": "#/components/schemas/PublicationApprovalRequested" - }, - "account": { - "type": "string" - }, - "detail-type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "region": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { + Content: + Fn::Sub: | + { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "PublicationApprovalRequested" + }, + "paths": {}, + "components": { + "schemas": { + "AWSEvent": { + "type": "object", + "required": [ + "detail-type", + "resources", + "detail", + "id", + "source", + "time", + "region", + "version", + "account" + ], + "x-amazon-events-detail-type": "PublicationApprovalRequested", + "x-amazon-events-source": "${EventRegistry.RegistryName}", + "properties": { + "detail": { + "$ref": "#/components/schemas/PublicationApprovalRequested" + }, + "account": { + "type": "string" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { "type": "string" } - }, - "source": { - "type": "string" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" } - } - }, - "PublicationApprovalRequested": { - "type": "object", - "required": [ - "images", - "address", - "listprice", - "contract", - "description", - "currency", - "property_id", - "status" - ], - "properties": { - "address": { - "$ref": "#/components/schemas/Address" - }, - "contract": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "description": { - "type": "string" - }, - "images": { - "type": "array", - "items": { + }, + "PublicationApprovalRequested": { + "type": "object", + "required": [ + "images", + "address", + "listprice", + "contract", + "description", + "currency", + "property_id", + "status" + ], + "properties": { + "address": { + "$ref": "#/components/schemas/Address" + }, + "contract": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "listprice": { + "type": "string" + }, + "property_id": { + "type": "string" + }, + "status": { "type": "string" } - }, - "listprice": { - "type": "string" - }, - "property_id": { - "type": "string" - }, - "status": { - "type": "string" } - } - }, - "Address": { - "type": "object", - "required": [ - "country", - "number", - "city", - "street" - ], - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "number": { - "type": "string" - }, - "street": { - "type": "string" + }, + "Address": { + "type": "object", + "required": [ + "country", + "number", + "city", + "street" + ], + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "number": { + "type": "string" + }, + "street": { + "type": "string" + } } } } } } - } diff --git a/unicorn_web/integration/subscriber-policies.yaml b/unicorn_web/integration/subscriber-policies.yaml index c7aaa03..ad90eb7 100644 --- a/unicorn_web/integration/subscriber-policies.yaml +++ b/unicorn_web/integration/subscriber-policies.yaml @@ -19,12 +19,15 @@ Resources: CrossServiceCreateRulePolicy: Type: AWS::Events::EventBusPolicy Properties: - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" - StatementId: !Sub "OnlyRulesForPropertiesServiceEvents-${Stage}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" + StatementId: + Fn::Sub: "OnlyRulesForWebServiceEvents-${Stage}" Statement: Effect: Allow Principal: - AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + AWS: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" Action: - events:PutRule - events:DeleteRule @@ -34,14 +37,15 @@ Resources: - events:PutTargets - events:RemoveTargets Resource: - - !Sub + - Fn::Sub: - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* - - eventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" + - eventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" Condition: StringEqualsIfExists: "events:creatorAccount": "${aws:PrincipalAccount}" StringEquals: "events:source": - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" "Null": "events:source": "false" diff --git a/unicorn_web/integration/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml index a3880c0..8078205 100644 --- a/unicorn_web/integration/subscriptions.yaml +++ b/unicorn_web/integration/subscriptions.yaml @@ -19,17 +19,20 @@ Resources: Properties: Name: unicorn.web-PublicationEvaluationCompleted Description: PublicationEvaluationCompleted subscription - EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + EventBusName: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: - - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" detail-type: - PublicationEvaluationCompleted State: ENABLED Targets: - Id: SendEventTo - Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" - RoleArn: !GetAtt UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn + Arn: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" + RoleArn: + Fn::GetAtt: UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events # from the publishing event bus, to the subscribing event bus (UnicornWebEventBusArn) @@ -48,9 +51,11 @@ Resources: Statement: - Effect: Allow Action: events:PutEvents - Resource: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" + Resource: + Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" Outputs: PublicationEvaluationCompletedSubscription: Description: Rule ARN for Property service event subscription - Value: !GetAtt PublicationEvaluationCompletedSubscriptionRule.Arn + Value: + Fn::GetAtt: PublicationEvaluationCompletedSubscriptionRule.Arn diff --git a/unicorn_web/samconfig.yaml b/unicorn_web/samconfig.yaml index 1e4629c..406b675 100644 --- a/unicorn_web/samconfig.yaml +++ b/unicorn_web/samconfig.yaml @@ -16,7 +16,9 @@ default: disable_rollback: true confirm_changeset: false fail_on_empty_changeset: false - capabilities: CAPABILITY_IAM + capabilities: + - CAPABILITY_IAM + - CAPABILITY_AUTO_EXPAND parameter_overrides: - "Stage=local" validate: @@ -31,51 +33,3 @@ default: local_start_lambda: parameters: warm_containers: EAGER - -schemas: - global: - parameters: - stack_name: uni-prop-local-web-schemas - s3_prefix: uni-prop-local-web-schemas - resolve_s3: true - deploy: - parameters: - template_file: integration/event-schemas.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - parameter_overrides: - - "Stage=local" - -subscriber-policies: - global: - parameters: - stack_name: uni-prop-local-web-subscriber-policies - s3_prefix: uni-prop-local-web-subscriber-policies - resolve_s3: true - deploy: - parameters: - template_file: ./integration/subscriber-policies.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - capabilities: CAPABILITY_IAM - parameter_overrides: - - "Stage=local" - -subscriptions: - global: - parameters: - stack_name: uni-prop-local-web-subscriptions - s3_prefix: uni-prop-local-web-subscriptions - resolve_s3: true - deploy: - parameters: - template_file: ./integration/subscriptions.yaml - confirm_changeset: false - fail_on_empty_changeset: false - on_failure: ROLLBACK - capabilities: CAPABILITY_IAM - parameter_overrides: - - "Stage=local" - \ No newline at end of file diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 40ed3ec..5eb9912 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 -Transform: +Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: > @@ -11,10 +11,11 @@ Metadata: cfn-lint: config: ignore_checks: - - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ - - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy - - WS2001 # Rule disabled because check does not support !ToJsonString transform - - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure + - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ + - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy + - WS2001 # Rule disabled because check does not support !ToJsonString transform + - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure + - W3002 Parameters: Stage: @@ -124,7 +125,7 @@ Resources: ScalingConfig: MaximumConcurrency: 5 - # Log group for the RequestApprovalFunction + # Log group for the RequestApprovalFunction RequestApprovalFunctionLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete @@ -414,7 +415,32 @@ Resources: ] } + #### CLOUDFORMATION NESTED STACKS + EventSchemasStack: + Type: AWS::Serverless::Application + Properties: + Location: "integration/event-schemas.yaml" + Parameters: + Stage: !Ref Stage + SubscriberPoliciesStack: + Type: AWS::Serverless::Application + DependsOn: + - UnicornWebEventBusParam + Properties: + Location: "integration/subscriber-policies.yaml" + Parameters: + Stage: !Ref Stage + SubscriptionsStack: + Type: AWS::Serverless::Application + DependsOn: + - UnicornWebEventBusArnParam + Properties: + Location: "integration/subscriptions.yaml" + Parameters: + Stage: !Ref Stage + Outputs: + #### API GATEWAY OUTPUTS BaseUrl: Description: Web service API endpoint Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}" @@ -422,6 +448,7 @@ Outputs: Description: Web service API endpoint Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/" + #### API ACTIONS OUTPUTS ApiSearchPropertiesByCity: Description: "GET request to list all properties in a given city" Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}" @@ -435,14 +462,17 @@ Outputs: Description: "GET request to get the full details of a single property" Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/properties/{country}/{city}/{street}/{number}" + #### SQS OUTPUTS IngestQueueUrl: Description: URL for the Ingest SQS Queue Value: !GetAtt UnicornWebIngestQueue.QueueUrl + #### DYNAMODB OUTPUTS WebTableName: Description: Name of the DynamoDB Table for Unicorn Web Value: !Ref WebTable + #### LAMBDA FUNCTIONS OUTPUTS SearchFunctionArn: Description: Search function ARN Value: !GetAtt SearchFunction.Arn @@ -453,5 +483,11 @@ Outputs: Description: Publication evaluation event handler function ARN Value: !GetAtt PublicationApprovedEventHandlerFunction.Arn - UnicornWebCatchAllLogGroupArn: - Value: !GetAtt UnicornWebCatchAllLogGroup.Arn + #### EVENT BRIDGE OUTPUTS + UnicornWebEventBusName: + Value: !GetAtt UnicornWebEventBus.Name + + #### CLOUDWATCH LOGS OUTPUTS + UnicornWebCatchAllLogGroupName: + Description: Log all events on the service's EventBridge Bus + Value: !Ref UnicornWebCatchAllLogGroup From 89019c57aea6cd4cbabfe51bd76636240ae81261 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Mon, 11 Sep 2023 10:01:27 +0000 Subject: [PATCH 73/87] Lint code (#43) * chore: remove unused code and forgotten comments * chore: reorder ENV vars; make LambdaContext mock generic to fit all functions * chore: lint Python files * test: add tracer and logger output on propertes_approval_sync_function methods * chore: reformat StateMachine ASL file * fix: update Stage name in shared stack * chore: renamed stack file for unicorn_shared --- unicorn_contracts/tests/unit/conftest.py | 18 +--- .../tests/unit/event_generator.py | 23 ----- unicorn_contracts/tests/unit/helper.py | 11 ++- .../tests/unit/test_contract_event_handler.py | 2 +- .../properties_approval_sync_function.py | 6 +- .../wait_for_contract_approval_function.py | 2 +- .../state_machine/property_approval.asl.yaml | 98 +++++++++---------- unicorn_shared/samconfig.yaml | 4 +- .../{uni-prop-shared.yaml => template.yaml} | 64 ++++++------ 9 files changed, 101 insertions(+), 127 deletions(-) rename unicorn_shared/{uni-prop-shared.yaml => template.yaml} (77%) diff --git a/unicorn_contracts/tests/unit/conftest.py b/unicorn_contracts/tests/unit/conftest.py index e1fa076..e5ef82d 100644 --- a/unicorn_contracts/tests/unit/conftest.py +++ b/unicorn_contracts/tests/unit/conftest.py @@ -18,18 +18,6 @@ def aws_credentials(): os.environ['AWS_SESSION_TOKEN'] = 'testing' -@pytest.fixture(scope='function') -def env_vars(): - os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' - os.environ['SERVICE_NAMESPACE']='unicorn.contracts' - os.environ['POWERTOOLS_SERVICE_NAME']='unicorn.contracts' - os.environ['POWERTOOLS_TRACE_DISABLED']='true' - os.environ['POWERTOOLS_LOGGER_LOG_EVENT']='Info' - os.environ['POWERTOOLS_LOGGER_SAMPLE_RATE']='0.1' - os.environ['POWERTOOLS_METRICS_NAMESPACE']='unicorn.contracts' - os.environ['LOG_LEVEL']='INFO' - - @pytest.fixture(scope='function') def dynamodb(aws_credentials): with mock_dynamodb(): @@ -51,12 +39,12 @@ def sqs(aws_credentials): @pytest.fixture(scope='function') def lambda_context(): context: LambdaContext = LambdaContext() - context._function_name="contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._function_name="contractsService-LambdaFunction-IWaQgsTEtLtX" context._function_version="$LATEST" - context._invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._invoked_function_arn="arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-LambdaFunction-IWaQgsTEtLtX" context._memory_limit_in_mb=128 context._aws_request_id="6f970d26-71d6-4c87-a196-9375f85c7b07" - context._log_group_name="/aws/lambda/contractsService-CreateContractFunction-IWaQgsTEtLtX" + context._log_group_name="/aws/lambda/contractsService-LambdaFunction-IWaQgsTEtLtX" context._log_stream_name="2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) # context._client_context=None diff --git a/unicorn_contracts/tests/unit/event_generator.py b/unicorn_contracts/tests/unit/event_generator.py index 334380c..66eff96 100644 --- a/unicorn_contracts/tests/unit/event_generator.py +++ b/unicorn_contracts/tests/unit/event_generator.py @@ -152,26 +152,3 @@ def sqs_event(messages: List[dict], }) return SQSEvent({ "Records": records }) - - - - -# { -# "Records": [ -# { -# "messageAttributes": { -# "HttpMethod": { -# "stringValue": "PUT", -# "stringListValues": [], -# "binaryListValues": [], -# "dataType": "String" -# } -# }, -# "md5OfMessageAttributes": "39c36267fdf9c8d354b1069e44662d24", -# "md5OfBody": "4bc398b7ae8e52d7f7eaed3cb76c12ef", -# "eventSource": "aws:sqs", -# "eventSourceARN": "arn:aws:sqs:ap-southeast-2:718758479978:uni-prop-local-contract-UnicornContractsIngestQueue-p5zisiK0Xbxn", -# "awsRegion": "ap-southeast-2" -# } -# ] -# } \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/helper.py b/unicorn_contracts/tests/unit/helper.py index a1f748a..44a5c8b 100644 --- a/unicorn_contracts/tests/unit/helper.py +++ b/unicorn_contracts/tests/unit/helper.py @@ -20,15 +20,16 @@ def return_env_vars_dict(k=None): env_dict = { "AWS_DEFAULT_REGION": "ap-southeast-2", + "EVENT_BUS": EVENTBUS_NAME, "DYNAMODB_TABLE": TABLE_NAME, - "EVENT_BUS": "test-eventbridge", - "LOG_LEVEL":"INFO", + "SERVICE_NAMESPACE": "unicorn.contracts", + "POWERTOOLS_LOGGER_CASE": "PascalCase", + "POWERTOOLS_SERVICE_NAME":"unicorn.contracts", + "POWERTOOLS_TRACE_DISABLED":"true", "POWERTOOLS_LOGGER_LOG_EVENT":"true", "POWERTOOLS_LOGGER_SAMPLE_RATE":"0.1", "POWERTOOLS_METRICS_NAMESPACE":"unicorn.contracts", - "POWERTOOLS_SERVICE_NAME":"unicorn.contracts", - "POWERTOOLS_TRACE_DISABLED":"true", - "SERVICE_NAMESPACE": "unicorn.contracts", + "LOG_LEVEL":"INFO", } env_dict |= k diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py index 3673dcc..25c6440 100644 --- a/unicorn_contracts/tests/unit/test_contract_event_handler.py +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -44,7 +44,7 @@ def test_valid_create_event(dynamodb, sqs, lambda_context): def test_valid_update_event(dynamodb, sqs, lambda_context): payload = load_event('update_contract_valid_1') event = sqs_event([{'body': payload, 'attributes': {'HttpMethod': 'PUT'}}]) - + # Loading function here so that mocking works correctly. from contracts_service import contract_event_handler # noqa: F401 # Reload is required to prevent function setup reuse from another test diff --git a/unicorn_properties/src/properties_service/properties_approval_sync_function.py b/unicorn_properties/src/properties_service/properties_approval_sync_function.py index 6030c85..2f4e111 100644 --- a/unicorn_properties/src/properties_service/properties_approval_sync_function.py +++ b/unicorn_properties/src/properties_service/properties_approval_sync_function.py @@ -28,7 +28,7 @@ @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore -@logger.inject_lambda_context(log_event=True) # type: ignore +@logger.inject_lambda_context(log_event=True) @tracer.capture_method def lambda_handler(event, context): """Functions processes DynamoDB Stream to detect changes in the contract status @@ -76,7 +76,7 @@ def lambda_handler(event, context): return result - +@tracer.capture_method def task_successful(task_token: str, contract_status: dict): """Send the token for a specified contract status back to Step Functions to continue workflow execution. @@ -89,9 +89,11 @@ def task_successful(task_token: str, contract_status: dict): Contract Status object to return to statemachine. """ output = {'Payload': contract_status} + logger.info(output) return sfn.send_task_success(taskToken=task_token, output=json.dumps(output)) +@tracer.capture_method def ddb_deserialize(dynamo_image: dict) -> dict: """Converts the DynamoDB stream object to json dict diff --git a/unicorn_properties/src/properties_service/wait_for_contract_approval_function.py b/unicorn_properties/src/properties_service/wait_for_contract_approval_function.py index f945db6..1103fb2 100644 --- a/unicorn_properties/src/properties_service/wait_for_contract_approval_function.py +++ b/unicorn_properties/src/properties_service/wait_for_contract_approval_function.py @@ -31,7 +31,7 @@ @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore -@logger.inject_lambda_context(log_event=True) # type: ignore +@logger.inject_lambda_context(log_event=True) @tracer.capture_method def lambda_handler(event, context): """Function checks to see whether the contract status exists and waits for APPROVAL diff --git a/unicorn_properties/state_machine/property_approval.asl.yaml b/unicorn_properties/state_machine/property_approval.asl.yaml index 4cdff43..d178f53 100644 --- a/unicorn_properties/state_machine/property_approval.asl.yaml +++ b/unicorn_properties/state_machine/property_approval.asl.yaml @@ -8,23 +8,23 @@ StartAt: VerifyContractExists States: VerifyContractExists: - Type: Task - Resource: arn:aws:states:::lambda:invoke - InputPath: $.detail - ResultPath: $.contract_exists_check - Parameters: - Payload: - Input.$: $ - FunctionName: ${ContractExistsChecker} - Next: CheckImageIntegrity - Catch: - - ErrorEquals: - - ContractStatusNotFoundException - Next: NotFound - Comment: >- - ContractExistsChecker checks to see if a contract for a specified - property exists. - + Type: Task + Resource: arn:aws:states:::lambda:invoke + InputPath: $.detail + ResultPath: $.contract_exists_check + Parameters: + Payload: + Input.$: $ + FunctionName: ${ContractExistsChecker} + Next: CheckImageIntegrity + Catch: + - ErrorEquals: + - ContractStatusNotFoundException + Next: NotFound + Comment: >- + ContractExistsChecker checks to see if a contract for a specified + property exists. + NotFound: Type: Fail @@ -50,7 +50,7 @@ States: unsafe content. ItemsPath: "$.detail.images" ResultPath: "$.imageModerations" - + CheckDescriptionSentiment: Type: Task Parameters: @@ -64,27 +64,27 @@ States: Type: Task Resource: arn:aws:states:::lambda:invoke ResultPath: "$.validation_check" - ResultSelector : + ResultSelector: validation_result.$: "$.Payload.validation_result" Parameters: Payload.$: $ FunctionName: ${ContentIntegrityValidator} Retry: - - ErrorEquals: - - Lambda.ServiceException - - Lambda.AWSLambdaException - - Lambda.SdkClientException - IntervalSeconds: 2 - MaxAttempts: 6 - BackoffRate: 2 + - ErrorEquals: + - Lambda.ServiceException + - Lambda.AWSLambdaException + - Lambda.SdkClientException + IntervalSeconds: 2 + MaxAttempts: 6 + BackoffRate: 2 Next: IsContentSafe IsContentSafe: Type: Choice Choices: - - Variable: $.validation_check.validation_result - StringEquals: PASS - Next: WaitForContractApproval + - Variable: $.validation_check.validation_result + StringEquals: PASS + Next: WaitForContractApproval Default: PublicationEvaluationCompletedDeclined PublicationEvaluationCompletedDeclined: @@ -92,16 +92,16 @@ States: Resource: arn:aws:states:::events:putEvents Parameters: Entries: - - Detail: - property_id.$: "$.detail.property_id" - evaluation_result: "DECLINED" - DetailType: PublicationEvaluationCompleted - EventBusName: ${EventBusName} - Source: ${ServiceName} + - Detail: + property_id.$: "$.detail.property_id" + evaluation_result: "DECLINED" + DetailType: PublicationEvaluationCompleted + EventBusName: ${EventBusName} + Source: ${ServiceName} Next: Declined Declined: Type: Succeed - + WaitForContractApproval: Type: Task Resource: arn:aws:states:::lambda:invoke.waitForTaskToken @@ -113,13 +113,13 @@ States: TaskToken.$: $$.Task.Token FunctionName: ${WaitForContractApproval} Retry: - - ErrorEquals: - - Lambda.ServiceException - - Lambda.AWSLambdaException - - Lambda.SdkClientException - IntervalSeconds: 2 - MaxAttempts: 6 - BackoffRate: 2 + - ErrorEquals: + - Lambda.ServiceException + - Lambda.AWSLambdaException + - Lambda.SdkClientException + IntervalSeconds: 2 + MaxAttempts: 6 + BackoffRate: 2 Next: PublicationEvaluationCompletedApproved Comment: ContractStatusChecker @@ -128,12 +128,12 @@ States: Resource: arn:aws:states:::events:putEvents Parameters: Entries: - - Detail: - property_id.$: "$.detail.property_id" - evaluation_result: "APPROVED" - DetailType: PublicationEvaluationCompleted - EventBusName: ${EventBusName} - Source: ${ServiceName} + - Detail: + property_id.$: "$.detail.property_id" + evaluation_result: "APPROVED" + DetailType: PublicationEvaluationCompleted + EventBusName: ${EventBusName} + Source: ${ServiceName} Next: Approved Approved: Type: Succeed diff --git a/unicorn_shared/samconfig.yaml b/unicorn_shared/samconfig.yaml index 1afe5f0..b77e6c9 100644 --- a/unicorn_shared/samconfig.yaml +++ b/unicorn_shared/samconfig.yaml @@ -8,11 +8,9 @@ default: resolve_s3: true deploy: parameters: - template_file: uni-prop-shared.yaml confirm_changeset: false fail_on_empty_changeset: false on_failure: ROLLBACK capabilities: CAPABILITY_IAM parameter_overrides: - - "Stage=Local" - + - "Stage=local" diff --git a/unicorn_shared/uni-prop-shared.yaml b/unicorn_shared/template.yaml similarity index 77% rename from unicorn_shared/uni-prop-shared.yaml rename to unicorn_shared/template.yaml index 75ca694..ef2e1b3 100644 --- a/unicorn_shared/uni-prop-shared.yaml +++ b/unicorn_shared/template.yaml @@ -1,34 +1,22 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: - - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: > Base infrastructure that will set up the central event bus and S3 image upload bucket. -#### PARAMETERS Parameters: Stage: Type: String - Default: Local + Default: local AllowedValues: - - Local - - Dev - - Prod + - local + - dev + - prod -#### MAPPINGS - Should be removed when values change to lower case -Mappings: - Stage: - Dev: - Value: dev - Prod: - Value: prod - Local: - Value: local - - -#### GLOBALS Globals: Function: Timeout: 15 @@ -43,29 +31,49 @@ Globals: service: Unicorn Base Infrastructure -#### RESOURCES Resources: - # S3 PROPERTY IMAGES BUCKET - UnicornPropertiesImagesBucket: - Type: AWS::S3::Bucket + #### SSM PARAMETERS + # Service Namespaces + UnicornContractsNamespaceParam: + Type: AWS::SSM::Parameter Properties: - BucketName: !Sub - - uni-prop-${st}-images-${AWS::AccountId} - - st: !FindInMap [Stage, !Ref Stage, Value] + Type: String + Name: !Sub /uni-prop/${Stage}/UnicornContractsNamespace + Value: "unicorn.contracts" + UnicornPropertiesNamespaceParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /uni-prop/${Stage}/UnicornPropertiesNamespace + Value: "unicorn.properties" + UnicornWebNamespaceParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: !Sub /uni-prop/${Stage}/UnicornWebNamespace + Value: "unicorn.web" + # Images S3 Bucket UnicornPropertiesImagesBucketParam: Type: AWS::SSM::Parameter Properties: Type: String - Name: !Sub /UniProp/${Stage}/ImagesBucket + Name: !Sub /uni-prop/${Stage}/ImagesBucket Value: !Ref UnicornPropertiesImagesBucket - # IMAGE UPLOAD CUSTOM RESOURCE FUNCTION + + #### S3 PROPERTY IMAGES BUCKET + UnicornPropertiesImagesBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub uni-prop-${Stage}-images-${AWS::AccountId} + + + #### IMAGE UPLOAD CUSTOM RESOURCE FUNCTION ImageUploadFunction: Type: AWS::Serverless::Function Properties: Handler: index.lambda_handler - Runtime: python3.9 Policies: - S3CrudPolicy: BucketName: !Ref UnicornPropertiesImagesBucket From 3b8e4e79ac83f523e3c53db3b8e248c792ccd132 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Tue, 12 Sep 2023 18:54:42 +1000 Subject: [PATCH 74/87] contract -> contracts --- unicorn_contracts/api.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_contracts/api.yaml b/unicorn_contracts/api.yaml index 4bf305d..573e180 100644 --- a/unicorn_contracts/api.yaml +++ b/unicorn_contracts/api.yaml @@ -4,7 +4,7 @@ info: version: "1.0.0" description: Unicorn Properties Contract Service API paths: - /contract: + /contracts: post: requestBody: content: From 3259fdc8504cdce7e8e338f159b5a9c682683fbd Mon Sep 17 00:00:00 2001 From: kechn Date: Wed, 13 Sep 2023 00:16:32 +1000 Subject: [PATCH 75/87] contract->contracts tests --- .../tests/integration/test_create_contract_apigw.py | 4 ++-- .../tests/integration/test_update_contract_apigw.py | 10 +++++----- .../tests/integration/test_request_approval_apigw.py | 4 ++-- unicorn_web/tests/integration/test_search_apigw.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/unicorn_contracts/tests/integration/test_create_contract_apigw.py b/unicorn_contracts/tests/integration/test_create_contract_apigw.py index eaedaf9..6baa29e 100644 --- a/unicorn_contracts/tests/integration/test_create_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_create_contract_apigw.py @@ -35,7 +35,7 @@ def test_create_contract_invalid_payload_1(self): """ payload = get_event_payload('create_contract_invalid_payload_1') - response = requests.post(f'{self.api_endpoint}contract', json = payload) + response = requests.post(f'{self.api_endpoint}contracts', json = payload) self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) @@ -45,7 +45,7 @@ def test_create_contract_valid_payload_1(self): payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contract', json=payload) + response = requests.post(f'{self.api_endpoint}contracts', json=payload) self.properties.append(payload['property_id']) self.assertEqual(response.status_code, 200) diff --git a/unicorn_contracts/tests/integration/test_update_contract_apigw.py b/unicorn_contracts/tests/integration/test_update_contract_apigw.py index f73d495..fedd2d7 100644 --- a/unicorn_contracts/tests/integration/test_update_contract_apigw.py +++ b/unicorn_contracts/tests/integration/test_update_contract_apigw.py @@ -34,7 +34,7 @@ def tearDown(self) -> None: # def test_update_existing_contract_invalid_payload_1(self): # payload = get_event_payload('update_existing_contract_invalid_payload_1') - # response = requests.put(f'{self.api_endpoint}contract', json=payload) + # response = requests.put(f'{self.api_endpoint}contracts', json=payload) # self.assertEqual(response.status_code, 400) @@ -43,11 +43,11 @@ def test_update_existing_contract_valid_payload(self): payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contract', json=payload) + response = requests.post(f'{self.api_endpoint}contracts', json=payload) self.properties.append(payload['property_id']) # Call API to update contract - response = requests.put(f'{self.api_endpoint}contract', json={'property_id': payload['property_id']}) + response = requests.put(f'{self.api_endpoint}contracts', json={'property_id': payload['property_id']}) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) @@ -70,7 +70,7 @@ def test_update_missing_contract_invalid_payload_1(self): "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" } - response = requests.put(f'{self.api_endpoint}contract', json=payload) + response = requests.put(f'{self.api_endpoint}contracts', json=payload) self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) @@ -80,6 +80,6 @@ def test_update_missing_contract_valid_payload(self): "property_id": "usa/some_other_town/street/878828" } - response = requests.put(f'{self.api_endpoint}contract', json=payload) + response = requests.put(f'{self.api_endpoint}contracts', json=payload) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) diff --git a/unicorn_web/tests/integration/test_request_approval_apigw.py b/unicorn_web/tests/integration/test_request_approval_apigw.py index eaedaf9..6baa29e 100644 --- a/unicorn_web/tests/integration/test_request_approval_apigw.py +++ b/unicorn_web/tests/integration/test_request_approval_apigw.py @@ -35,7 +35,7 @@ def test_create_contract_invalid_payload_1(self): """ payload = get_event_payload('create_contract_invalid_payload_1') - response = requests.post(f'{self.api_endpoint}contract', json = payload) + response = requests.post(f'{self.api_endpoint}contracts', json = payload) self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) @@ -45,7 +45,7 @@ def test_create_contract_valid_payload_1(self): payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contract', json=payload) + response = requests.post(f'{self.api_endpoint}contracts', json=payload) self.properties.append(payload['property_id']) self.assertEqual(response.status_code, 200) diff --git a/unicorn_web/tests/integration/test_search_apigw.py b/unicorn_web/tests/integration/test_search_apigw.py index eaedaf9..6baa29e 100644 --- a/unicorn_web/tests/integration/test_search_apigw.py +++ b/unicorn_web/tests/integration/test_search_apigw.py @@ -35,7 +35,7 @@ def test_create_contract_invalid_payload_1(self): """ payload = get_event_payload('create_contract_invalid_payload_1') - response = requests.post(f'{self.api_endpoint}contract', json = payload) + response = requests.post(f'{self.api_endpoint}contracts', json = payload) self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) @@ -45,7 +45,7 @@ def test_create_contract_valid_payload_1(self): payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contract', json=payload) + response = requests.post(f'{self.api_endpoint}contracts', json=payload) self.properties.append(payload['property_id']) self.assertEqual(response.status_code, 200) From 779805f42257ababfd5811595eab97b1f4e28c40 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 15:02:35 +1000 Subject: [PATCH 76/87] test: remove partially-implemented Integration tests --- .../tests/integration/__init__.py | 74 ------------ .../put_event_contract_status_changed.json | 8 -- ..._event_publication_approval_requested.json | 8 -- .../test_contract_status_changed_event.py | 109 ------------------ ...st_publication_approval_requested_event.py | 1 - unicorn_web/tests/integration/__init__.py | 79 ------------- .../create_contract_invalid_payload_1.json | 5 - .../create_contract_valid_payload_1.json | 10 -- ...e_existing_contract_invalid_payload_1.json | 6 - ...ate_existing_contract_valid_payload_1.json | 3 - ...te_missing_contract_invalid_payload_1.json | 5 - ...date_missing_contract_valid_payload_1.json | 3 - .../test_request_approval_apigw.py | 60 ---------- .../tests/integration/test_search_apigw.py | 60 ---------- .../transformations/ddb_contract.jq | 14 --- 15 files changed, 445 deletions(-) delete mode 100644 unicorn_properties/tests/integration/__init__.py delete mode 100644 unicorn_properties/tests/integration/events/put_event_contract_status_changed.json delete mode 100644 unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json delete mode 100644 unicorn_properties/tests/integration/test_contract_status_changed_event.py delete mode 100644 unicorn_properties/tests/integration/test_publication_approval_requested_event.py delete mode 100644 unicorn_web/tests/integration/__init__.py delete mode 100644 unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json delete mode 100644 unicorn_web/tests/integration/events/create_contract_valid_payload_1.json delete mode 100644 unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json delete mode 100644 unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json delete mode 100644 unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json delete mode 100644 unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json delete mode 100644 unicorn_web/tests/integration/test_request_approval_apigw.py delete mode 100644 unicorn_web/tests/integration/test_search_apigw.py delete mode 100644 unicorn_web/tests/integration/transformations/ddb_contract.jq diff --git a/unicorn_properties/tests/integration/__init__.py b/unicorn_properties/tests/integration/__init__.py deleted file mode 100644 index e54c0af..0000000 --- a/unicorn_properties/tests/integration/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -from typing import Iterator - -import json -from pathlib import Path - -import boto3 -from arnparse import arnparse -from yaml import load, Loader - - -#### CONSTANTS -DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' -STACK_OUTPUTS = dict() -EVENTS_DIR = Path(__file__).parent / 'events' - - -#### AWS SDK Objects -cfn = boto3.client('cloudformation') -# cwl = boto3.client('logs') -# ddb = boto3.client('dynamodb') - - -def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: - with open(samconfig, 'r') as f: - conf = load(f, Loader=Loader) - stack_name = conf['default']['global']['parameters']['stack_name'] - - return stack_name - - -def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: - """ - Get the value of an output - """ - - if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): - try: - response = cfn.describe_stacks(StackName=stack_name) - except Exception as e: - raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e - - outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} - STACK_OUTPUTS[stack_name] = outputs - - try: - return outputs[output_name] - except KeyError as e: - raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e - - -def get_event_payload(file) -> dict: - return json.load(open(EVENTS_DIR / f'{file}.json', 'r')) - - -# def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: -# group_name = arnparse(eb_log_group_arn).resource - -# # Get the CW LogStream with the latest log messages -# stream_response = cwl.describe_log_streams(logGroupName=group_name, orderBy='LastEventTime',descending=True,limit=3) -# latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] -# # Fetch log events from that stream -# responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] - -# # Filter log events that match the required `property_id` -# for response in responses: -# for event in response["events"]: -# if (ev := json.loads(event["message"])).get('detail', {}).get('property_id', '') == property_id: -# yield ev - - -# def clean_ddb(table_name, property_id): -# ddb.delete_item(TableName=table_name, Key={ 'property_id': { 'S': property_id } }) diff --git a/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json b/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json deleted file mode 100644 index 3d395a1..0000000 --- a/unicorn_properties/tests/integration/events/put_event_contract_status_changed.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "Source": "unicorn.contracts", - "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": \"199\",\"property_id\":\"bbb\",\"contract_status\":\"APPROVED\"}", - "DetailType": "ContractStatusChanged", - "EventBusName": "Dev-UnicornPropertiesEventBus" - } -] \ No newline at end of file diff --git a/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json b/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json deleted file mode 100644 index 3791334..0000000 --- a/unicorn_properties/tests/integration/events/put_event_publication_approval_requested.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "EventBusName": "Dev-UnicornPropertiesEventBus", - "Source": "unicorn.properties.web", - "DetailType": "PublicationApprovalRequested", - "Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\": \"sale\",\"listprice\": 200,\"currency\": \"SPL\",\"images\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}" - } -] \ No newline at end of file diff --git a/unicorn_properties/tests/integration/test_contract_status_changed_event.py b/unicorn_properties/tests/integration/test_contract_status_changed_event.py deleted file mode 100644 index c4e9f4b..0000000 --- a/unicorn_properties/tests/integration/test_contract_status_changed_event.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -import json -from time import sleep -from datetime import datetime - -import boto3 -from unittest import TestCase - -from . import get_stack_output - - -evb = boto3.client('events') -ddb = boto3.client('dynamodb') - - -properties_bus = get_stack_output('UnicornPropertiesEventBusName') - - -def send_contract_status_changed(property_id, contract_id, contract_status): - return evb.put_events(Entries=[{ - 'Source': 'unicorn.contracts', - 'DetailType': 'ContractStatusChanged', - 'EventBusName': properties_bus, - 'Detail': json.dumps({ - 'contract_last_modified_on': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), - 'property_id': property_id, - 'contract_id': contract_id, - 'contract_status': contract_status}), - }]) - - -def send_publication_approval_requested(property_id, contract_id, contract_status): - return evb.put_events(Entries=[{ - 'Source': 'unicorn.web', - 'DetailType': 'PublicationApprovalRequested', - 'EventBusName': properties_bus, - 'Detail': json.dumps({ - 'contract_last_modified_on': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), - 'property_id': property_id, - 'contract_id': contract_id, - 'contract_status': contract_status}), - }]) - - - -# { -# "DetailType": "PublicationApprovalRequested", -# "Source": "unicorn.properties.web", -# "EventBusName": "UnicornPropertiesEventBus-Local", -# "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"prop1_exterior1.jpg\",\"prop1_interior1.jpg\",\"prop1_interior2.jpg\",\"prop1_interior3.jpg\"]}" -# } - -# x = { -# "property_id":"usa/anytown/main-street/222", -# "address":{ -# "country":"USA", -# "city":"Anytown", -# "street":"Main Street", -# "number":222 -# }, -# "description":"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", -# "contract":"sale", -# "listprice":200, -# "currency":"SPL", -# "images":[ -# "prop1_exterior1.jpg", -# "prop1_interior1.jpg", -# "prop1_interior2.jpg", -# "prop1_interior3.jpg" -# ] -# } - - - - -class TestCreateContract(TestCase): - properties_bus: str - contract_status_table_name: str - - def setUp(self) -> None: - self.contract_status_table_name = get_stack_output('ContractStatusTableName') - - - def test_contract_status_draft(self): - evp_resp = send_contract_status_changed(property_id='usa/anytown/main-street/111', - contract_id='f2bedc80-3dc8-4544-9140-9b606d71a6ee', - contract_status='DRAFT') - - sleep(2) - - ddb_resp = ddb.get_item(TableName=self.contract_status_table_name, - Key={'property_id': {'S': 'usa/anytown/main-street/111'}}) - - assert ddb_resp['Item']['contract_status']['S'] == 'DRAFT' - assert ddb_resp['Item'].get('sfn_wait_approved_task_token', None) is None - - def test_contract_status_approved(self): - evp_resp = send_contract_status_changed(property_id='usa/anytown/main-street/111', - contract_id='f2bedc80-3dc8-4544-9140-9b606d71a6ee', - contract_status='APPROVED') - - sleep(2) - - ddb_resp = ddb.get_item(TableName=self.contract_status_table_name, - Key={'property_id': {'S': 'usa/anytown/main-street/111'}}) - - assert ddb_resp['Item']['contract_status']['S'] == 'APPROVED' - assert ddb_resp['Item'].get('sfn_wait_approved_task_token', None) is not None diff --git a/unicorn_properties/tests/integration/test_publication_approval_requested_event.py b/unicorn_properties/tests/integration/test_publication_approval_requested_event.py deleted file mode 100644 index 336183e..0000000 --- a/unicorn_properties/tests/integration/test_publication_approval_requested_event.py +++ /dev/null @@ -1 +0,0 @@ -# Event triggers SFN diff --git a/unicorn_web/tests/integration/__init__.py b/unicorn_web/tests/integration/__init__.py deleted file mode 100644 index 006dbae..0000000 --- a/unicorn_web/tests/integration/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Iterator - -import json -from pathlib import Path - -import boto3 -from arnparse import arnparse -from yaml import load, Loader - - -#### CONSTANTS -DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / 'samconfig.yaml' -STACK_OUTPUTS = dict() -EVENTS_DIR = Path(__file__).parent / 'events' - - -#### AWS SDK Objects -cfn = boto3.client('cloudformation') -cwl = boto3.client('logs') -ddb = boto3.client('dynamodb') - - -def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: - with open(samconfig, 'r') as f: - conf = load(f, Loader=Loader) - stack_name = conf['default']['global']['parameters']['stack_name'] - - return stack_name - - -def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: - """ - Get the value of an output - """ - - if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): - try: - response = cfn.describe_stacks(StackName=stack_name) - except Exception as e: - raise Exception(f"Cannot find stack {stack_name}. \n" f'Please make sure stack "{stack_name}" exists.') from e - - outputs = {o['OutputKey']: o['OutputValue'] for o in response["Stacks"][0]["Outputs"]} - STACK_OUTPUTS[stack_name] = outputs - - try: - return outputs[output_name] - except KeyError as e: - raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e - - -def get_event_payload(file) -> dict: - return json.load(open(EVENTS_DIR / f'{file}.json', 'r')) - - -def override_payload_number(p: dict, number: int) -> dict: - p['address']['number'] = number - a = p["address"] - p['property_id'] = f'{a["country"]}/{a["city"]}/{a["street"]}/{a["number"]}'.replace(' ', '-').lower() - return p - - -def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: - group_name = arnparse(eb_log_group_arn).resource - - # Get the CW LogStream with the latest log messages - stream_response = cwl.describe_log_streams(logGroupName=group_name, orderBy='LastEventTime',descending=True,limit=3) - latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] - # Fetch log events from that stream - responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] - - # Filter log events that match the required `property_id` - for response in responses: - for event in response["events"]: - if (ev := json.loads(event["message"])).get('detail', {}).get('property_id', '') == property_id: - yield ev - - -def clean_ddb(table_name, property_id): - ddb.delete_item(TableName=table_name, Key={ 'property_id': { 'S': property_id } }) diff --git a/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json deleted file mode 100644 index 1f352a3..0000000 --- a/unicorn_web/tests/integration/events/create_contract_invalid_payload_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json deleted file mode 100644 index 8a9abf2..0000000 --- a/unicorn_web/tests/integration/events/create_contract_valid_payload_1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "address": { - "country": "USA", - "city": "Anytown", - "street": "Main Street", - "number": 123 - }, - "seller_name": "John Smith", - "property_id": "usa/anytown/main-street/123" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json deleted file mode 100644 index 154d225..0000000 --- a/unicorn_web/tests/integration/events/update_existing_contract_invalid_payload_1.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "property_id": "usa/anytown/main-street/123", - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json deleted file mode 100644 index ea61b51..0000000 --- a/unicorn_web/tests/integration/events/update_existing_contract_valid_payload_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "property_id": "usa/anytown/main-street/123" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json b/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json deleted file mode 100644 index 1f352a3..0000000 --- a/unicorn_web/tests/integration/events/update_missing_contract_invalid_payload_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "add": "St.1 , Building 10", - "sell": "John Smith", - "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json b/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json deleted file mode 100644 index 5793374..0000000 --- a/unicorn_web/tests/integration/events/update_missing_contract_valid_payload_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "property_id": "usa/some_other_town/street/878828" -} \ No newline at end of file diff --git a/unicorn_web/tests/integration/test_request_approval_apigw.py b/unicorn_web/tests/integration/test_request_approval_apigw.py deleted file mode 100644 index 6baa29e..0000000 --- a/unicorn_web/tests/integration/test_request_approval_apigw.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -from typing import List - -from time import sleep -from random import randint - -import requests -from unittest import TestCase - -from . import get_stack_output, get_cw_logs_values, clean_ddb -from . import get_event_payload, override_payload_number - - -class TestCreateContract(TestCase): - api_endpoint: str - eb_log_group: str - contracts_table: str - properties: List[str] - - def setUp(self) -> None: - self.api_endpoint = get_stack_output('ApiUrl') - self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") - self.contracts_table = get_stack_output('ContractsTableName') - self.properties = list() - - def tearDown(self) -> None: - for i in self.properties: - clean_ddb(self.contracts_table, i) - - - def test_create_contract_invalid_payload_1(self): - """ - Call the API Gateway endpoint and check the response - """ - - payload = get_event_payload('create_contract_invalid_payload_1') - response = requests.post(f'{self.api_endpoint}contracts', json = payload) - self.assertEqual(response.status_code, 400) - self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) - - - def test_create_contract_valid_payload_1(self): - prop_number = randint(1, 9999) - payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) - - # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contracts', json=payload) - self.properties.append(payload['property_id']) - - self.assertEqual(response.status_code, 200) - self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) - - sleep(5) - try: - eb_event = next(get_cw_logs_values(self.eb_log_group, payload['property_id'])) - except Exception: - raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') - - self.assertEqual(eb_event['detail']['contract_status'], "DRAFT") diff --git a/unicorn_web/tests/integration/test_search_apigw.py b/unicorn_web/tests/integration/test_search_apigw.py deleted file mode 100644 index 6baa29e..0000000 --- a/unicorn_web/tests/integration/test_search_apigw.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 -from typing import List - -from time import sleep -from random import randint - -import requests -from unittest import TestCase - -from . import get_stack_output, get_cw_logs_values, clean_ddb -from . import get_event_payload, override_payload_number - - -class TestCreateContract(TestCase): - api_endpoint: str - eb_log_group: str - contracts_table: str - properties: List[str] - - def setUp(self) -> None: - self.api_endpoint = get_stack_output('ApiUrl') - self.eb_log_group = get_stack_output('UnicornContractsCatchAllLogGroupArn').rstrip(":*") - self.contracts_table = get_stack_output('ContractsTableName') - self.properties = list() - - def tearDown(self) -> None: - for i in self.properties: - clean_ddb(self.contracts_table, i) - - - def test_create_contract_invalid_payload_1(self): - """ - Call the API Gateway endpoint and check the response - """ - - payload = get_event_payload('create_contract_invalid_payload_1') - response = requests.post(f'{self.api_endpoint}contracts', json = payload) - self.assertEqual(response.status_code, 400) - self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) - - - def test_create_contract_valid_payload_1(self): - prop_number = randint(1, 9999) - payload = override_payload_number(get_event_payload('create_contract_valid_payload_1'), prop_number) - - # Call API to create new Contract - response = requests.post(f'{self.api_endpoint}contracts', json=payload) - self.properties.append(payload['property_id']) - - self.assertEqual(response.status_code, 200) - self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) - - sleep(5) - try: - eb_event = next(get_cw_logs_values(self.eb_log_group, payload['property_id'])) - except Exception: - raise Exception(f'Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}') - - self.assertEqual(eb_event['detail']['contract_status'], "DRAFT") diff --git a/unicorn_web/tests/integration/transformations/ddb_contract.jq b/unicorn_web/tests/integration/transformations/ddb_contract.jq deleted file mode 100644 index 54e3bf8..0000000 --- a/unicorn_web/tests/integration/transformations/ddb_contract.jq +++ /dev/null @@ -1,14 +0,0 @@ -.Item | { - property_id: .property_id.S, - contract_id: .contract_id.S, - seller_name: .seller_name.S, - address: { - country: .address.M.country.S, - number: .address.M.number.N, - city: .address.M.city.S, - street: .address.M.street.S, - }, - contract_status: .contract_status.S, - contract_created: .contract_created.S, - contract_last_modified_on: .contract_last_modified_on.S -} | del(..|nulls) From 8b39a4ae940a7a3c8c6fbba6f1753e84e76829d7 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 15:03:22 +1000 Subject: [PATCH 77/87] chore: Bring files inline with Dotnet, Java and Typescript --- unicorn_contracts/Makefile | 21 ++++---- unicorn_contracts/api.yaml | 4 +- .../integration/event-schemas.yaml | 2 +- unicorn_contracts/template.yaml | 49 ++++++++++--------- unicorn_properties/Makefile | 13 ++--- .../integration/subscriptions.yaml | 47 +++++++++--------- unicorn_properties/pyproject.toml | 3 +- unicorn_properties/template.yaml | 9 ++++ unicorn_web/Makefile | 20 ++++---- unicorn_web/pyproject.toml | 2 +- unicorn_web/template.yaml | 23 ++++++--- 11 files changed, 103 insertions(+), 90 deletions(-) diff --git a/unicorn_contracts/Makefile b/unicorn_contracts/Makefile index 3777ca5..0c44055 100644 --- a/unicorn_contracts/Makefile +++ b/unicorn_contracts/Makefile @@ -19,15 +19,8 @@ build: deps: poetry install -deploy: build - sam deploy --no-confirm-changeset - -# NOTE: [2023-05-09] This is a fix for installing Poetry dependencies in GitHub Actions -ci_init: - poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt --with dev - poetry run pip install -r src/requirements.txt - poetry install -n - +deploy: deps build + sam deploy #### Tests test: unit-test integration-test @@ -66,16 +59,22 @@ sync: sam sync --stack-name $(stackName) --watch logs: - sam logs --stack-name $(stackName) -t + sam logs -t clean: find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true - rm -rf .pytest_cache/ .aws-sam/ htmlcov/ .coverage || true + rm -rf .pytest_cache/ .aws-sam/ || true delete: sam delete --stack-name $(stackName) --no-prompts +# NOTE: [2023-05-09] This is a fix for installing Poetry dependencies in GitHub Actions +ci_init: + poetry export --without-hashes --format=requirements.txt --output=src/requirements.txt --with dev + poetry run pip install -r src/requirements.txt + poetry install -n + #### Helper Functions define runif diff --git a/unicorn_contracts/api.yaml b/unicorn_contracts/api.yaml index 573e180..82b6331 100644 --- a/unicorn_contracts/api.yaml +++ b/unicorn_contracts/api.yaml @@ -22,7 +22,7 @@ paths: x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: - Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + Fn::GetAtt: [UnicornContractsApiIntegrationRole, Arn] httpMethod: "POST" uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" @@ -84,7 +84,7 @@ paths: x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: credentials: - Fn::GetAtt: [ContractsApiIntegrationRole, Arn] + Fn::GetAtt: [UnicornContractsApiIntegrationRole, Arn] httpMethod: "POST" uri: "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" diff --git a/unicorn_contracts/integration/event-schemas.yaml b/unicorn_contracts/integration/event-schemas.yaml index 9ac7084..d976f24 100644 --- a/unicorn_contracts/integration/event-schemas.yaml +++ b/unicorn_contracts/integration/event-schemas.yaml @@ -47,7 +47,7 @@ Resources: - Fn::GetAtt: EventRegistry.RegistryArn - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" - ContractApprovedEventSchema: + ContractStatusChangedEventSchema: Type: AWS::EventSchemas::Schema Properties: Type: 'OpenApi3' diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index a264ee4..6d9342b 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -61,6 +61,7 @@ Globals: POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage @@ -150,13 +151,33 @@ Resources: project: !FindInMap [Constants, ProjectName, Value] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + UnicornContractsApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + # API Gateway Account Configuration, to enable Logs to be sent to CloudWatch ContractsApiGwAccountConfig: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt UnicornContractsApiGwAccountConfigRole.Arn # API GW IAM roles - ContractsApiIntegrationRole: + UnicornContractsApiGwAccountConfigRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: apigateway.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + + UnicornContractsApiIntegrationRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -175,26 +196,6 @@ Resources: - sqs:GetQueueUrl Resource: !GetAtt UnicornContractsIngestQueue.Arn - # API GW Cloudwatch Log Group - UnicornContractsApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - - UnicornContractsApiGwAccountConfigRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Principal: - Service: apigateway.amazonaws.com - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs - #### INGEST QUEUES # Queue API Gateway requests to be processed by ContractEventHandlerFunction UnicornContractsIngestQueue: @@ -359,8 +360,6 @@ Resources: StartingPosition: LATEST OnPartialBatchItemFailure: AUTOMATIC_BISECT BatchSize: 1 - # DeadLetterConfig: - # Arn: String FilterCriteria: Filters: - Pattern: !ToJsonString @@ -369,7 +368,6 @@ Resources: NewImage: contract_status: S: [DRAFT, APPROVED] - # Enrichment: arn:aws:execute-api:us-east-1:123456789123:53eo2i89p9/*/POST/pets Target: !GetAtt UnicornContractsEventBus.Arn TargetParameters: EventBridgeEventBusParameters: @@ -411,12 +409,15 @@ Resources: Resource: !GetAtt UnicornContractsEventBus.Arn #### CLOUDFORMATION NESTED STACKS + # CloudFormation Stack with the Contracts Service Event Registry and Schemas EventSchemasStack: Type: AWS::Serverless::Application Properties: Location: "integration/event-schemas.yaml" Parameters: Stage: !Ref Stage + + # CloudFormation Stack with the Cross-service EventBus policy for Contracts Service SubscriberPoliciesStack: Type: AWS::Serverless::Application DependsOn: diff --git a/unicorn_properties/Makefile b/unicorn_properties/Makefile index 1bb920b..d3458b5 100644 --- a/unicorn_properties/Makefile +++ b/unicorn_properties/Makefile @@ -2,6 +2,9 @@ stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.yaml) +#### Test Variables + + #### Build/Deploy Tasks build: sam validate --lint @@ -12,30 +15,28 @@ build: deps: poetry install -deploy: build +deploy: deps build sam deploy #### Tests -test: unit-test integration-test +test: unit-test unit-test: poetry run pytest tests/unit/ -integration-test: deps - poetry run pytest tests/integration/ #### Utilities sync: sam sync --stack-name $(stackName) --watch logs: - sam logs --stack-name $(stackName) -t + sam logs -t clean: find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true - rm -rf .pytest_cache/ .aws-sam/ htmlcov/ .coverage || true + rm -rf .pytest_cache/ .aws-sam/ || true delete: sam delete --stack-name $(stackName) --no-prompts diff --git a/unicorn_properties/integration/subscriptions.yaml b/unicorn_properties/integration/subscriptions.yaml index 871e0fa..63d6388 100644 --- a/unicorn_properties/integration/subscriptions.yaml +++ b/unicorn_properties/integration/subscriptions.yaml @@ -13,7 +13,6 @@ Parameters: - prod Resources: - #### UNICORN CONTRACTS EVENT SUBSCRIPTIONS ContractStatusChangedSubscriptionRule: Type: AWS::Events::Rule @@ -36,25 +35,25 @@ Resources: Fn::GetAtt: UnicornPropertiesSubscriptionRole.Arn #### UNICORN WEB EVENT SUBSCRIPTIONS - PublicationApprovalRequestedSubscriptionRule: - Type: AWS::Events::Rule - Properties: - Name: unicorn.properties-PublicationApprovalRequested - Description: Publication evaluation completed subscription - EventBusName: - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" - EventPattern: - source: - - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" - detail-type: - - PublicationApprovalRequested - State: ENABLED - Targets: - - Id: SendEventTo - Arn: - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" - RoleArn: - Fn::GetAtt: UnicornPropertiesSubscriptionRole.Arn + # PublicationApprovalRequestedSubscriptionRule: + # Type: AWS::Events::Rule + # Properties: + # Name: unicorn.properties-PublicationApprovalRequested + # Description: Publication evaluation completed subscription + # EventBusName: + # Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" + # EventPattern: + # source: + # - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + # detail-type: + # - PublicationApprovalRequested + # State: ENABLED + # Targets: + # - Id: SendEventTo + # Arn: + # Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + # RoleArn: + # Fn::GetAtt: UnicornPropertiesSubscriptionRole.Arn # This IAM role allows EventBridge to assume the permissions necessary to send events @@ -83,7 +82,7 @@ Outputs: Value: Fn::GetAtt: ContractStatusChangedSubscriptionRule.Arn - PublicationApprovalRequestedSubscription: - Description: Rule ARN for Web service event subscription - Value: - Fn::GetAtt: PublicationApprovalRequestedSubscriptionRule.Arn + # PublicationApprovalRequestedSubscription: + # Description: Rule ARN for Web service event subscription + # Value: + # Fn::GetAtt: PublicationApprovalRequestedSubscriptionRule.Arn diff --git a/unicorn_properties/pyproject.toml b/unicorn_properties/pyproject.toml index 93c4733..1992009 100644 --- a/unicorn_properties/pyproject.toml +++ b/unicorn_properties/pyproject.toml @@ -16,7 +16,6 @@ aws-xray-sdk = "^2.12.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" -# pytest-mock = "^3.11.1" requests = "^2.31.0" moto = "^4.1.13" importlib-metadata = "^6.8.0" @@ -30,7 +29,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" addopts = "-ra -vv -W ignore::UserWarning" -testpaths = ["tests/unit", "tests/integration"] +testpaths = ["tests/unit"] [tool.ruff] line-length = 150 diff --git a/unicorn_properties/template.yaml b/unicorn_properties/template.yaml index 2722823..bba6a4f 100644 --- a/unicorn_properties/template.yaml +++ b/unicorn_properties/template.yaml @@ -14,6 +14,8 @@ Metadata: - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ - ES6000 # Rule disabled because SQS DLOs don't need a RedrivePolicy - E0001 # Rule disabled because cfn-lint cannot parse SAM Policy templates without arguments (ComprehendBasicAccessPolicy, RekognitionDetectOnlyPolicy) + - WS2001 # Rule disabled because check does not support !ToJsonString transform + - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure Parameters: Stage: @@ -52,11 +54,13 @@ Globals: CONTRACT_STATUS_TABLE: !Ref ContractStatusTable EVENT_BUS: !Ref UnicornPropertiesEventBus SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + POWERTOOLS_LOGGER_CASE: PascalCase POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" + POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage @@ -441,12 +445,15 @@ Resources: } #### CLOUDFORMATION NESTED STACKS + # CloudFormation Stack with the Properties Service Event Registry and Schemas EventSchemasStack: Type: AWS::Serverless::Application Properties: Location: "integration/event-schemas.yaml" Parameters: Stage: !Ref Stage + + # CloudFormation Stack with the Cross-service EventBus policy for Properties Service SubscriberPoliciesStack: Type: AWS::Serverless::Application DependsOn: @@ -455,6 +462,8 @@ Resources: Location: "integration/subscriber-policies.yaml" Parameters: Stage: !Ref Stage + + # CloudFormation Stack with the Cross-service EventBus Rules for Properties Service SubscriptionsStack: Type: AWS::Serverless::Application DependsOn: diff --git a/unicorn_web/Makefile b/unicorn_web/Makefile index 9465c2f..2033b04 100644 --- a/unicorn_web/Makefile +++ b/unicorn_web/Makefile @@ -6,10 +6,8 @@ stackName := $(shell yq -oy '.default.global.parameters.stack_name' samconfig.y apiUrl = $(call cf_output,$(stackName),ApiUrl) -#### Build/Deploy Tasks -deps: - poetry install +#### Build/Deploy Tasks build: sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules @@ -17,19 +15,19 @@ build: poetry export -f requirements.txt --without-hashes --output src/search_service/requirements.txt sam build -c $(DOCKER_OPTS) -deploy: build +deps: + poetry install + +deploy: deps build sam deploy #### Tests -test: unit-test integration-test +test: unit-test unit-test: deps poetry run pytest tests/unit/ -integration-test: deps - poetry run pytest tests/integration/ - curl-test: $(call mcurl,GET,search/usa/anytown) $(call mcurl,GET,search/usa/anytown/main-street) @@ -42,15 +40,15 @@ sync: sam sync --stack-name $(stackName) --watch logs: - sam logs --stack-name $(stackName) -t + sam logs -t clean: find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true - rm -rf .pytest_cache/ .aws-sam/ htmlcov/ .coverage || true + rm -rf .pytest_cache/ .aws-sam/ || true delete: - sam delete --no-prompts --region "$$(aws configure get region)" + sam delete --stack-name $(stackName) --no-prompts # NOTE: [2023-05-09] This is a fix for installing Poetry dependencies in GitHub Actions ci_init: diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index 3b84396..23c0c4c 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -31,7 +31,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "7.0" addopts = "-ra -vv -W ignore::UserWarning" -testpaths = ["tests/unit", "tests/integration"] +testpaths = ["tests/unit"] [tool.ruff] line-length = 150 diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 5eb9912..04e1d89 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -62,6 +62,7 @@ Globals: POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default POWERTOOLS_METRICS_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default + LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Tags: stage: !Ref Stage project: !FindInMap [Constants, ProjectName, Value] @@ -207,6 +208,15 @@ Resources: project: !FindInMap [Constants, ProjectName, Value] namespace: Unicorn Web Service + # API GW CloudWatch Logs Group, logs all requests from API Gateway + UnicornWebApiLogGroup: + Type: AWS::Logs::LogGroup + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Properties: + RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] + + # API Gateway Account Configuration, to enable Logs to be sent to CloudWatch UnicornWebApiGwAccountConfig: Type: AWS::ApiGateway::Account Properties: @@ -252,14 +262,6 @@ Resources: - lambda:InvokeFunction Resource: !GetAtt SearchFunction.Arn - # API GW CloudWatch Logs Group, logs all requests from API Gateway - UnicornWebApiLogGroup: - Type: AWS::Logs::LogGroup - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Properties: - RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] - #### INGEST QUEUES # Queue API Gateway requests to be processed by RequestApprovalFunction UnicornWebIngestQueue: @@ -416,12 +418,15 @@ Resources: } #### CLOUDFORMATION NESTED STACKS + # CloudFormation Stack with the Web Service Event Registry and Schemas EventSchemasStack: Type: AWS::Serverless::Application Properties: Location: "integration/event-schemas.yaml" Parameters: Stage: !Ref Stage + + # CloudFormation Stack with the Cross-service EventBus policy for Web Service SubscriberPoliciesStack: Type: AWS::Serverless::Application DependsOn: @@ -430,6 +435,8 @@ Resources: Location: "integration/subscriber-policies.yaml" Parameters: Stage: !Ref Stage + + # CloudFormation Stack with the Cross-service EventBus Rules for Web Service SubscriptionsStack: Type: AWS::Serverless::Application DependsOn: From 4da2d6ed167b490e5ce3f3755a4a40d90006746e Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 17:51:29 +1000 Subject: [PATCH 78/87] test: fixing EventBus name in PublicationEvaluationCompleted event payload of unicorn_web --- .../eventbridge/put_event_publication_evaluation_completed.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json b/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json index 0dd3470..6b715f6 100644 --- a/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json +++ b/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json @@ -3,6 +3,6 @@ "Source": "unicorn.properties", "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}", "DetailType": "PublicationEvaluationCompleted", - "EventBusName": "UnicornPropertiesEventBus-Local" + "EventBusName": "UnicornWebBus-local" } ] \ No newline at end of file From 9d8f9b26d772954723c0f8673b7b6282f60ced0a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 17:52:14 +1000 Subject: [PATCH 79/87] bug: wrong Event source filter parameter in unicorn_web subscriptions --- unicorn_web/integration/subscriptions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_web/integration/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml index 8078205..ad183bc 100644 --- a/unicorn_web/integration/subscriptions.yaml +++ b/unicorn_web/integration/subscriptions.yaml @@ -23,7 +23,7 @@ Resources: Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" EventPattern: source: - - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" + - Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" detail-type: - PublicationEvaluationCompleted State: ENABLED From 8375a09680ea0e8ea476a947d7649145b61b9d41 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:00:46 +1000 Subject: [PATCH 80/87] chore: add log statements in unicorn_web's request_approval_function for publish_event --- unicorn_web/src/approvals_service/request_approval_function.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 34ae9e1..5a33e5d 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -46,8 +46,10 @@ def publish_event(detail_type, resources, detail): 'DetailType': detail_type, 'Resources': resources, 'Detail': json.dumps(detail)} + logger.info(entry) response = event_bridge.put_events(Entries=[entry]) + logger.info(response) except ClientError as e: error_msg = f"Unable to send event to Event Bus: {e}" logger.error(error_msg) From 50e5aabedf3e921d4806e649545ed857a2afe649 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:01:50 +1000 Subject: [PATCH 81/87] bug: remove message attribute HttpMethod filter --- .../src/approvals_service/request_approval_function.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 5a33e5d..8d435c2 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -134,9 +134,4 @@ def request_approval(raw_data: dict): def lambda_handler(event: SQSEvent, context: LambdaContext): # Multiple records can be delivered in a single event for record in event.records: - http_method = record.message_attributes.get('HttpMethod', {}).get('stringValue') - - if http_method == 'POST': - request_approval(record.json_body) - else: - raise Exception(f'Unable to handle HttpMethod {http_method}') + request_approval(record.json_body) From b5fe7165c0d746f39f9cd0a5c0ea34257536a9ee Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:02:20 +1000 Subject: [PATCH 82/87] bug: unicorn_web requestApproval should only filter out properties in APPROVED state --- unicorn_web/src/approvals_service/request_approval_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_web/src/approvals_service/request_approval_function.py b/unicorn_web/src/approvals_service/request_approval_function.py index 8d435c2..6a117f3 100644 --- a/unicorn_web/src/approvals_service/request_approval_function.py +++ b/unicorn_web/src/approvals_service/request_approval_function.py @@ -109,7 +109,7 @@ def request_approval(raw_data: dict): # Get property details from database item = get_property(pk=pk, sk=sk) - if (status := item.pop('status')) in [ 'APPROVED', 'DECLINED', 'PENDING' ]: + if (status := item.pop('status')) in [ 'APPROVED' ]: logger.info(f"Property '{property_id}' is already {status}; no action taken") return From cc70ed1c7f1b8d38f82073198c64ef8ec17fb257 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:03:37 +1000 Subject: [PATCH 83/87] refactor: add schema package to pyproject --- unicorn_web/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index 23c0c4c..ac3e848 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -6,6 +6,7 @@ authors = ["Amazon Web Services"] packages = [ { include = "approvals_service", from = "src" }, { include = "search_service", from = "src" }, + { include = "schema", from = "src" }, ] [tool.poetry.dependencies] From 8425d64fc69923a38b650a9dc8b3ae4851ad8cb3 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:04:22 +1000 Subject: [PATCH 84/87] chore: add missing global env var to unicorn_web, to bring it inline with other languages --- unicorn_web/template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index 04e1d89..ac983c4 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -56,6 +56,7 @@ Globals: DYNAMODB_TABLE: !Ref WebTable EVENT_BUS: !Ref UnicornWebEventBus SERVICE_NAMESPACE: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" + POWERTOOLS_LOGGER_CASE: PascalCase POWERTOOLS_SERVICE_NAME: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebNamespace}}" POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default From ac2c62e695cec8ae784c8550b23c370acbffff80 Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:05:09 +1000 Subject: [PATCH 85/87] bug: wrong parameter being used for unicorn_web's PublicationApprovedEventHandlerFunction --- unicorn_web/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index ac983c4..f6194c5 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -157,7 +157,7 @@ Resources: EventBusName: !Ref UnicornWebEventBus Pattern: source: - - !Ref UnicornWebEventBus + - !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesNamespace}}" detail-type: - PublicationEvaluationCompleted From 70871b4519136302b538bc70f1aa267d427546aa Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Wed, 13 Sep 2023 18:05:47 +1000 Subject: [PATCH 86/87] refactor: change CodeUri of all Lambda functions in unicorn_web to be src/. This is to fix Pythons import paths --- unicorn_web/Makefile | 3 +-- unicorn_web/template.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/unicorn_web/Makefile b/unicorn_web/Makefile index 2033b04..ca572a5 100644 --- a/unicorn_web/Makefile +++ b/unicorn_web/Makefile @@ -11,8 +11,7 @@ apiUrl = $(call cf_output,$(stackName),ApiUrl) build: sam validate --lint cfn-lint template.yaml -a cfn_lint_serverless.rules - poetry export -f requirements.txt --without-hashes --output src/approvals_service/requirements.txt - poetry export -f requirements.txt --without-hashes --output src/search_service/requirements.txt + poetry export -f requirements.txt --without-hashes --output src/requirements.txt sam build -c $(DOCKER_OPTS) deps: diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml index f6194c5..d6c02d4 100644 --- a/unicorn_web/template.yaml +++ b/unicorn_web/template.yaml @@ -91,8 +91,8 @@ Resources: SearchFunction: Type: AWS::Serverless::Function Properties: - CodeUri: src/search_service/ - Handler: property_search_function.lambda_handler + CodeUri: src/ + Handler: search_service.property_search_function.lambda_handler Policies: - DynamoDBReadPolicy: TableName: !Ref WebTable @@ -110,8 +110,8 @@ Resources: RequestApprovalFunction: Type: AWS::Serverless::Function Properties: - CodeUri: src/approvals_service/ - Handler: request_approval_function.lambda_handler + CodeUri: src/ + Handler: approvals_service.request_approval_function.lambda_handler Policies: - EventBridgePutEventsPolicy: EventBusName: !Ref UnicornWebEventBus @@ -140,8 +140,8 @@ Resources: PublicationApprovedEventHandlerFunction: Type: AWS::Serverless::Function Properties: - CodeUri: src/approvals_service/ - Handler: publication_approved_event_handler.lambda_handler + CodeUri: src/ + Handler: approvals_service.publication_approved_event_handler.lambda_handler Policies: - DynamoDBWritePolicy: TableName: !Ref WebTable From 118e4808586a58d32551c3ba409c6ae8cbdd616a Mon Sep 17 00:00:00 2001 From: Igor Gentil Date: Thu, 14 Sep 2023 09:30:39 +1000 Subject: [PATCH 87/87] docs: add API Gateway CW Logs group tag --- unicorn_contracts/template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml index 6d9342b..6e6fa51 100644 --- a/unicorn_contracts/template.yaml +++ b/unicorn_contracts/template.yaml @@ -151,6 +151,7 @@ Resources: project: !FindInMap [Constants, ProjectName, Value] namespace: !Sub "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsNamespace}}" + # API GW Cloudwatch Log Group UnicornContractsApiLogGroup: Type: AWS::Logs::LogGroup UpdateReplacePolicy: Delete