From d08edf0d3b2a4435253b7811b9761015f90056a8 Mon Sep 17 00:00:00 2001 From: Tom McLaughlin Date: Sun, 29 Dec 2024 21:56:25 +0000 Subject: [PATCH] Initial commit via Backstage --- .cfnlintrc | 2 + .github/workflows/feature-branch.yml | 74 +++++++++++ .github/workflows/main.yml | 74 +++++++++++ .gitignore | 81 ++++++++++++ Pipfile | 39 ++++++ README.md | 13 ++ catalog-info.yaml | 16 +++ cfn-parameters.json | 7 + cfn-tags.json | 5 + data/handlers/ListAccounts/data.json | 1 + data/handlers/ListAccounts/data.schema.json | 7 + data/handlers/ListAccounts/event.json | 13 ++ data/handlers/ListAccounts/event.schema.json | 57 ++++++++ pytest.ini | 2 + samconfig.toml | 9 ++ src/__init__.py | 1 + src/common/Makefile | 6 + src/common/common/__init__.py | 0 src/common/common/model/__init__.py | 0 src/common/common/test/__init__.py | 0 src/common/common/test/aws/__init__.py | 19 +++ src/common/common/util/__init__.py | 0 src/common/common/util/dataclasses.py | 9 ++ src/common/setup.py | 27 ++++ src/handlers/ListAccounts/__init__.py | 0 src/handlers/ListAccounts/function.py | 37 ++++++ src/handlers/ListAccounts/requirements.txt | 2 + src/handlers/__init__.py | 1 + template.yaml | 62 +++++++++ tests/conftest.py | 9 ++ tests/ete/__init__.py | 0 tests/integration/__init__.py | 0 tests/integration/handlers/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/handlers/ListAccounts/__init__.py | 0 .../handlers/ListAccounts/test_function.py | 123 ++++++++++++++++++ tests/unit/handlers/__init__.py | 0 37 files changed, 696 insertions(+) create mode 100644 .cfnlintrc create mode 100644 .github/workflows/feature-branch.yml create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 README.md create mode 100644 catalog-info.yaml create mode 100644 cfn-parameters.json create mode 100644 cfn-tags.json create mode 100644 data/handlers/ListAccounts/data.json create mode 100644 data/handlers/ListAccounts/data.schema.json create mode 100644 data/handlers/ListAccounts/event.json create mode 100644 data/handlers/ListAccounts/event.schema.json create mode 100644 pytest.ini create mode 100644 samconfig.toml create mode 100644 src/__init__.py create mode 100644 src/common/Makefile create mode 100644 src/common/common/__init__.py create mode 100644 src/common/common/model/__init__.py create mode 100644 src/common/common/test/__init__.py create mode 100644 src/common/common/test/aws/__init__.py create mode 100644 src/common/common/util/__init__.py create mode 100644 src/common/common/util/dataclasses.py create mode 100644 src/common/setup.py create mode 100644 src/handlers/ListAccounts/__init__.py create mode 100644 src/handlers/ListAccounts/function.py create mode 100644 src/handlers/ListAccounts/requirements.txt create mode 100644 src/handlers/__init__.py create mode 100644 template.yaml create mode 100644 tests/conftest.py create mode 100644 tests/ete/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/handlers/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/handlers/ListAccounts/__init__.py create mode 100644 tests/unit/handlers/ListAccounts/test_function.py create mode 100644 tests/unit/handlers/__init__.py diff --git a/.cfnlintrc b/.cfnlintrc new file mode 100644 index 0000000..ab48754 --- /dev/null +++ b/.cfnlintrc @@ -0,0 +1,2 @@ +ignore_checks: + - E3034 # Template too short \ No newline at end of file diff --git a/.github/workflows/feature-branch.yml b/.github/workflows/feature-branch.yml new file mode 100644 index 0000000..d63df17 --- /dev/null +++ b/.github/workflows/feature-branch.yml @@ -0,0 +1,74 @@ +name: Feature Branch +on: + workflow_dispatch: + push: + branches: + - feature/* + - fix/* + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + + - name: Setup Python environment + uses: ServerlessOpsIO/gha-setup-python@v1 + with: + python_version: '3.13' + + - name: Run tests + run: pipenv run test-unit + + - name: Assume AWS credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + + - name: Install AWS SAM + uses: aws-actions/setup-sam@v2 + + - name: Validate template + run: sam validate --lint + + - name: Build deployment artifact + run: sam build + + - name: Store artifacts + uses: ServerlessOpsIO/gha-store-artifacts@v1 + with: + use_aws_sam: true + + deploy: + needs: + - build + + environment: production + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + with: + checkout_artifact: true + + - name: Assume AWS credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + deploy_aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + + - name: Deploy via AWS SAM + uses: ServerlessOpsIO/gha-deploy-aws-sam@v1 + with: + aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + env_json: ${{ toJson(env) }} + secrets_json: ${{ toJson(secrets) }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f491aaf --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,74 @@ +name: Main + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + + - name: Setup Python environment + uses: ServerlessOpsIO/gha-setup-python@v1 + with: + python_version: '3.13' + + - name: Run tests + run: pipenv run test-unit + + - name: Assume AWS credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + + - name: Install AWS SAM + uses: aws-actions/setup-sam@v2 + + - name: Validate template + run: sam validate --lint + + - name: Build deployment artifact + run: sam build + + - name: Store artifacts + uses: ServerlessOpsIO/gha-store-artifacts@v1 + with: + use_aws_sam: true + + deploy: + needs: + - build + + environment: production + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + with: + checkout_artifact: true + + - name: Assume AWS credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + deploy_aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + + - name: Deploy via AWS SAM + uses: ServerlessOpsIO/gha-deploy-aws-sam@v1 + with: + aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + env_json: ${{ toJson(env) }} + secrets_json: ${{ toJson(secrets) }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ca90b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Dev +.mypy_cache/ + +# pyenv / environments +.python-version +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.settings/ +.project +.pydevproject +.vscode/ +*.code-workspace +.idea/ + +# Mac Cruft +.DS_Store +Thumbs.db + +# IDE +.vscode +!.vscode/launch.json +!.vscode/tasks.json + +# AWS SAM +.aws-sam/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..d91ace3 --- /dev/null +++ b/Pipfile @@ -0,0 +1,39 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[requires] +python_version = "3.13" + +[packages] +common = {editable = true, path = "src/common"} +aws-lambda-powertools = "*" + +[dev-packages] +boto3-stubs = { extras = [ "sns", ], version = "*"} +cfn-lint = "*" +flake8 = "*" +genson = "*" +jsonschema = "*" +json2python-models = "*" +moto = { extras = [ "sns", ], version = "*"} +mypy = "*" +pylint = "*" +pytest = "*" +pytest-cov = "*" +pytest-flake8 = "*" +pytest-mock = "*" +pytest-mypy = "*" +pytest-pylint = "*" +tox = "*" + +[scripts] +test = "pytest -vv --cov src --cov-report term-missing --cov-fail-under 95 tests" +test-unit = "pytest -vv --cov src --cov-report term-missing --cov-fail-under 95 tests/unit" +test-int = "pytest -vv --cov src --cov-report term-missing --cov-fail-under 95 tests/integration" +test-ete = "pytest -vv --cov src --cov-report term-missing --cov-fail-under 95 tests/ete" +flake8 = "pytest -vv --flake8" +pylint = "pytest -vv --pylint" +mypy = "pytest -vv --mypy" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfbbc13 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# backstage / backstage-aws-resource-collector + +Collect resources from AWS accounts + + +## New Project Getting Started +This repository was generated from a template intended to get a new API up and running quickly. This section will cover different aspects of the newly created project as well as areas that may need to be modified to meet the specific needs of a new project. + + +### Code +When starting a new project the first place to start with adapting the code to meet the needs of a new project is the [`src/common/common/model/.py`](src/common/common/model/.py) file. This file contains interfaces for data and DDB table items. + +Start by modifying the dataclasses to match the shape of your data. Optionally you can choose to replace that interface with one from another module if you're working with a pre-existing data model. The existing interface definition was chosen simply to make the project work out of the box. diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..9da7aae --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-aws-resource-collector + description: Collect resources from AWS accounts + annotations: + github.com/project-slug: ServerlessOpsIO/backstage-aws-resource-collector +spec: + type: api + lifecycle: production + owner: group:backstage + system: system:backstage + providesApis: + - resource:backstage-aws-resource-collector + diff --git a/cfn-parameters.json b/cfn-parameters.json new file mode 100644 index 0000000..04fe972 --- /dev/null +++ b/cfn-parameters.json @@ -0,0 +1,7 @@ + +{ + "Domain": "devtools", + "System": "backstage", + "Component": $env.GITHUB_REPOSITORY_NAME_PART_SLUG_CS, + "CodeBranch": $env.GITHUB_REF_SLUG_CS +} \ No newline at end of file diff --git a/cfn-tags.json b/cfn-tags.json new file mode 100644 index 0000000..c1d4f7e --- /dev/null +++ b/cfn-tags.json @@ -0,0 +1,5 @@ +{ + "org:domain": "devtools", + "org:system": "backstage", + "org:component": $env.GITHUB_REPOSITORY_NAME_PART_SLUG_CS +} \ No newline at end of file diff --git a/data/handlers/ListAccounts/data.json b/data/handlers/ListAccounts/data.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/handlers/ListAccounts/data.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/handlers/ListAccounts/data.schema.json b/data/handlers/ListAccounts/data.schema.json new file mode 100644 index 0000000..b47d428 --- /dev/null +++ b/data/handlers/ListAccounts/data.schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Event", + "type": "object", + "properties": {}, + "additionalProperties": true +} diff --git a/data/handlers/ListAccounts/event.json b/data/handlers/ListAccounts/event.json new file mode 100644 index 0000000..225d4ed --- /dev/null +++ b/data/handlers/ListAccounts/event.json @@ -0,0 +1,13 @@ +{ + "version": "0", + "id": "bb675cb9-4cf4-4e6e-8a2e-9f17d21465d8", + "detail-type": "Scheduled Event", + "source": "aws.scheduler", + "account": "123456789012", + "time": "2024-12-13T22:46:36Z", + "region": "us-east-1", + "resources": [ + "arn:aws:scheduler:us-east-1:123456789012:schedule/default/ScheduledProcessorFunctionSchedule" + ], + "detail": "{}" +} \ No newline at end of file diff --git a/data/handlers/ListAccounts/event.schema.json b/data/handlers/ListAccounts/event.schema.json new file mode 100644 index 0000000..170e79c --- /dev/null +++ b/data/handlers/ListAccounts/event.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/EventbridgeEvent", + "definitions": { + "EventbridgeEvent": { + "required": [ + "version", + "id", + "detail-type", + "source", + "account", + "time", + "region", + "resources", + "detail" + ], + "properties": { + "account": { + "type": "string" + }, + "detail": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "detail-type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "items": { + "type": "string" + }, + "type": "array" + }, + "source": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + } + } +} \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/samconfig.toml b/samconfig.toml new file mode 100644 index 0000000..b44a750 --- /dev/null +++ b/samconfig.toml @@ -0,0 +1,9 @@ +version = 0.1 +[default] + +[default.deploy] + +[default.deploy.parameters] +stack_name = "backstage-aws-resource-collector" +confirm_changeset = false +capabilities = "CAPABILITY_NAMED_IAM" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c296546 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# This has been made a package to aid local dev module reolution for test. \ No newline at end of file diff --git a/src/common/Makefile b/src/common/Makefile new file mode 100644 index 0000000..55f5239 --- /dev/null +++ b/src/common/Makefile @@ -0,0 +1,6 @@ +build-CommonLayer: + python3.13 ./setup.py build + pip3.13 wheel -w tmp -e . + mkdir $(ARTIFACTS_DIR)/python + pip3.13 install tmp/common-* --target $(ARTIFACTS_DIR)/python/ + diff --git a/src/common/common/__init__.py b/src/common/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/common/model/__init__.py b/src/common/common/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/common/test/__init__.py b/src/common/common/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/common/test/aws/__init__.py b/src/common/common/test/aws/__init__.py new file mode 100644 index 0000000..6b320ce --- /dev/null +++ b/src/common/common/test/aws/__init__.py @@ -0,0 +1,19 @@ +''' +Resources for testing in AWS. +''' +from collections import namedtuple +from typing import Any, Dict, List, Tuple, Union + +import boto3 + +def create_lambda_function_context(function_name: str, object_name: str = 'LambdaContext') -> Tuple: + '''Return a named tuple representing a context object''' + context_info = { + 'aws_request_id': '00000000-0000-0000-0000-000000000000', + 'function_name': function_name, + 'invoked_function_arn': 'arn:aws:lambda:us-east-1:012345678910:function:{}'.format(function_name), + 'memory_limit_in_mb': 128 + } + + Context = namedtuple(object_name, context_info.keys()) + return Context(*context_info.values()) \ No newline at end of file diff --git a/src/common/common/util/__init__.py b/src/common/common/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/common/util/dataclasses.py b/src/common/common/util/dataclasses.py new file mode 100644 index 0000000..c48a8fb --- /dev/null +++ b/src/common/common/util/dataclasses.py @@ -0,0 +1,9 @@ +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.typing import LambdaContext +from dataclasses import asdict +from typing import Any, Callable, Dict + +@lambda_handler_decorator +def lambda_dataclass_response(handler: Callable[..., Any], event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]: + response = handler(event, context) + return asdict(response) \ No newline at end of file diff --git a/src/common/setup.py b/src/common/setup.py new file mode 100644 index 0000000..ee28da0 --- /dev/null +++ b/src/common/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name='common', + version='0.0.1', + description='backstage-aws-resource-collector common code', + author='NBCUniversal', + license='Apache License 2.0', + packages=find_packages(exclude=['tests.*', 'tests']), + keywords="backstage-aws-resource-collector service", + python_requires='>=3.13', + include_package_data=True, + install_requires=[ + 'aws_lambda_powertools', + 'boto3' + ], + classifiers=[ + 'Environment :: Console', + 'Environment :: Other Environment', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.13', + ] +) + diff --git a/src/handlers/ListAccounts/__init__.py b/src/handlers/ListAccounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/handlers/ListAccounts/function.py b/src/handlers/ListAccounts/function.py new file mode 100644 index 0000000..973f346 --- /dev/null +++ b/src/handlers/ListAccounts/function.py @@ -0,0 +1,37 @@ + +'''List AWS accounts''' +import os +import boto3 + +from aws_lambda_powertools.logging import Logger +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.data_classes import ( + event_source, + EventBridgeEvent +) + +LOGGER = Logger(utc=True) + +SNS_CLIENT = boto3.client('sns') +SNS_TOPIC_ARN = os.environ.get('SNS_TOPIC_ARN', 'UNSET') + + + +def _main(data) -> None: + '''Main work of function''' + # Transform data + + # Send data to destination + + return + + +@LOGGER.inject_lambda_context +@event_source(data_class=EventBridgeEvent) +def handler(event: EventBridgeEvent, context: LambdaContext) -> None: + '''Event handler''' + LOGGER.debug('Event', extra={"message_object": event}) + + _main(event.detail) + + return diff --git a/src/handlers/ListAccounts/requirements.txt b/src/handlers/ListAccounts/requirements.txt new file mode 100644 index 0000000..b69be1f --- /dev/null +++ b/src/handlers/ListAccounts/requirements.txt @@ -0,0 +1,2 @@ +-e src/common/ +aws_lambda_powertools \ No newline at end of file diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py new file mode 100644 index 0000000..c296546 --- /dev/null +++ b/src/handlers/__init__.py @@ -0,0 +1 @@ +# This has been made a package to aid local dev module reolution for test. \ No newline at end of file diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..802431e --- /dev/null +++ b/template.yaml @@ -0,0 +1,62 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: | + Collect resources from AWS accounts + +Parameters: + Domain: + Type: String + Description: 'Application Domain' + + System: + Type: String + Description: 'Application System' + + Component: + Type: String + Description: 'Application Component' + + CodeBranch: + Type: String + Description: "Name of deployment branch" + + + +Globals: + Function: + Runtime: python3.13 + Timeout: 5 + MemorySize: 128 + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: !Ref AWS::StackName + + +Resources: + # Functions + ListAccountsFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./src/handlers/ListAccounts + Handler: function.handler + Description: List AWS accounts + Events: + Schedule: + Type: ScheduleV2 + Properties: + ScheduleExpression: rate(15 minutes) + Policies: + - SNSPublishMessagePolicy: + TopicName: !Ref DestinationSnsTopic + Environment: + Variables: + SNS_TOPIC_ARN: !Ref DestinationSnsTopic + + + + + DestinationSnsTopic: + Type: AWS::SNS::Topic + Properties: + DisplayName: ListAccounts Destination + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a273456 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import os +import sys + +# NOTE: Hack so we can test against local functions without installing them +# into the venv as pytest expects +# +# ref: https://github.com/pytest-dev/pytest/issues/2421#issuecomment-403724503 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + diff --git a/tests/ete/__init__.py b/tests/ete/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/handlers/__init__.py b/tests/integration/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/handlers/ListAccounts/__init__.py b/tests/unit/handlers/ListAccounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/handlers/ListAccounts/test_function.py b/tests/unit/handlers/ListAccounts/test_function.py new file mode 100644 index 0000000..4ae4a76 --- /dev/null +++ b/tests/unit/handlers/ListAccounts/test_function.py @@ -0,0 +1,123 @@ +'''Test ListAccounts''' +from dataclasses import asdict +import json +import jsonschema +import os +from types import ModuleType +from typing import Generator + +import pytest +from pytest_mock import MockerFixture + + +import boto3 +from mypy_boto3_sns import SNSClient +from moto import mock_aws + +from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent +from aws_lambda_powertools.utilities.typing import LambdaContext +from common.test.aws import create_lambda_function_context + +FN_NAME = 'ListAccounts' +DATA_DIR = './data' +FUNC_DATA_DIR = os.path.join(DATA_DIR, 'handlers', FN_NAME) +EVENT = os.path.join(FUNC_DATA_DIR, 'event.json') +EVENT_SCHEMA = os.path.join(FUNC_DATA_DIR, 'event.schema.json') +DATA = os.path.join(FUNC_DATA_DIR, 'data.json') +DATA_SCHEMA = os.path.join(FUNC_DATA_DIR, 'data.schema.json') + +### Fixtures + +# FIXME: Need to handle differences between powertools event classes and the Event class +# Event +@pytest.fixture() +def mock_event(e=EVENT) -> EventBridgeEvent: + '''Return a function event''' + with open(e) as f: + return EventBridgeEvent(json.load(f)) + +@pytest.fixture() +def event_schema(schema=EVENT_SCHEMA): + '''Return an event schema''' + with open(schema) as f: + return json.load(f) +# AWS Clients +# +# NOTE: Mocking AWS services must also be done before importing the function. +@pytest.fixture() +def aws_credentials() -> None: + '''Mocked AWS Credentials for moto.''' + os.environ["AWS_ACCESS_KEY_ID"] = "testing" + os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" + os.environ["AWS_SECURITY_TOKEN"] = "testing" + os.environ["AWS_SESSION_TOKEN"] = "testing" + os.environ["AWS_DEFAULT_REGION"] = "us-east-1" + +@pytest.fixture() +def mocked_aws(aws_credentials): + '''Mock all AWS interactions''' + with mock_aws(): + yield + + +@pytest.fixture() +def mock_sns_client(mocked_aws) -> Generator[SNSClient, None, None]: + sns_client = boto3.client('sns') + yield sns_client + +@pytest.fixture() +def mock_sns_topic_name(mock_sns_client) -> str: + '''Create a mock resource''' + mock_topic_name = 'MockTopic' + mock_sns_client.create_topic(Name=mock_topic_name) + return mock_topic_name + +# Function +@pytest.fixture() +def mock_context(function_name=FN_NAME): + '''context object''' + return create_lambda_function_context(function_name) + +@pytest.fixture() +def mock_fn( + mock_sns_topic_name: str, + mocker: MockerFixture +) -> Generator[ModuleType, None, None]: + '''Return mocked function''' + import src.handlers.ListAccounts.function as fn + + # NOTE: use mocker to mock any top-level variables outside of the handler function. + mocker.patch( + 'src.handlers.ListAccounts.function.SNS_TOPIC_ARN', + mock_sns_topic_name + ) + + yield fn + + +### Data validation tests +# FIXME: Need to handle differences between powertools event classes and the Event class +def test_validate_event(mock_event, event_schema): + '''Test event against schema''' + jsonschema.Draft7Validator(mock_event._data, event_schema) + + +### Code Tests +def test__main( + mock_fn: ModuleType, + mock_data +): + '''Test _main function''' + mock_fn._main(mock_data) + + +def test_handler( + mock_fn: ModuleType, + mock_context, + mock_event: EventBridgeEvent, + mock_data + mock_sns_client: SNSClient, +): + '''Test calling handler''' + # Call the function + mock_fn.handler(mock_event, mock_context) \ No newline at end of file diff --git a/tests/unit/handlers/__init__.py b/tests/unit/handlers/__init__.py new file mode 100644 index 0000000..e69de29