diff --git a/CloudFormation/CustomResources/getfromjson/.coveragerc b/CloudFormation/CustomResources/getfromjson/.coveragerc new file mode 100644 index 00000000..be90b332 --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/.coveragerc @@ -0,0 +1,11 @@ +[report] +fail_under = 100 +show_missing = True + +[run] +branch = True +include = + src/*.py +omit = + src/__init__.py + src/tests/* diff --git a/CloudFormation/CustomResources/getfromjson/.gitignore b/CloudFormation/CustomResources/getfromjson/.gitignore new file mode 100644 index 00000000..c2d25f7b --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/.gitignore @@ -0,0 +1,3 @@ +venv/ +__pycache__/ +.coverage diff --git a/CloudFormation/CustomResources/getfromjson/README.md b/CloudFormation/CustomResources/getfromjson/README.md new file mode 100644 index 00000000..ce93e53f --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/README.md @@ -0,0 +1,266 @@ +# getfromjson + + +## Overview + +`getfromjson` is a module for Python that is meant to run in an [AWS +Lambda](https://aws.amazon.com/lambda/) function that, in turn, backs +one (or more) [AWS +CloudFormation](https://aws.amazon.com/cloudformation/) [custom +resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) +that you declare and use to get a given value out of an input JSON +data structure and an input search argument you both provide. + +For more information on Lambda-backed custom resources, see +[Lambda-backed custom +resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html). + +There are two parts you'll need to set up. First, you set up the +infrastructure needed to support `getfromjson`: you do this with the +`src/getfromjson.yml` CloudFormation template, that describes the +following resources: + +- the Lambda function and the `getfromjson.py` module for Python, that + will back custom resource consumers; this function will be + responsible for returning values from an input JSON data structure + and search argument, that you both provide; + +- the [AWS Identity and Access Management + (IAM)](https://aws.amazon.com/iam/) [execution + role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) + for the Lambda function; + +- the [Amazon CloudWatch + Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html) + log group for the Lambda function. + +The _Setup_ section, further down in this document, shows you how to +create resources above, in a given AWS account and AWS region, by +using the template mentioned earlier to create a CloudFormation stack. + +Next, you consume the Lambda function, that you previously created in +a given AWS account and AWS region, with custom resources that you +declare in other CloudFormation templates (that you'll use to create +new stacks) where you'll pass in both JSON data and a search argument +for the data. The `example-templates/` directory contains samples that +illustrate how to consume `getfromjson` with Lambda-backed custom +resources; the following snippet shows you an overview on how to +consume, in another template, the Lambda function (the example below +consumes custom resources' values in the `Outputs` section of the +template, but you can consume such values also from properties of +other resources you describe in the `Resources` section of the +template): + +``` +Resources: + GetFromJsonCustomResourceSampleGetFromList: + Type: Custom::GetFromJson + Properties: + ServiceTimeout: 1 + ServiceToken: !ImportValue Custom-GetFromJson + json_data: '["test0", "test1", "test2"]' + search: '[2]' + + GetFromJsonCustomResourceSampleGetFromMap: + Type: Custom::GetFromJson + Properties: + ServiceTimeout: 1 + ServiceToken: !ImportValue Custom-GetFromJson + json_data: '{"test": {"test1": ["x", "y"]}}' + search: '["test"]["test1"][1]' + +Outputs: + GetFromJsonCustomResourceSampleGetFromListValue: + Value: !GetAtt GetFromJsonCustomResourceSampleGetFromList.Data + + GetFromJsonCustomResourceSampleGetFromMapValue: + Value: !GetAtt GetFromJsonCustomResourceSampleGetFromMap.Data +``` + +whereas the `GetFromJsonCustomResourceSampleGetFromListValue` and +`GetFromJsonCustomResourceSampleGetFromMapValue` outputs, once you'll +create the stack, will show `test2` and `y` respectively as the +returned values. + +Note also the `ServiceToken: !ImportValue Custom-GetFromJson` line, +that the example above uses to tell the custom resource what is the +[Amazon Resource Name +(ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) +of the Lambda function that backs the custom resource itself. The +Lambda function's ARN is +[exported](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html) +in the `getfromjson` template with the `Custom-GetFromJson` export +name: in the example above, you use the `Fn::ImportValue` [intrinsic +function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) +to reference the ARN from the export. + +For more information on CloudFormation custom resources in a +CloudFormation template, see the `AWS::CloudFormation::CustomResource` +[reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-customresource.html). + + +## Input and output: values and limits + +The following are supported input and output values: + +- input: + + - `json_data`: + + - `json_data` maximum length: 4,096 bytes; + + - map keys can contain alphanumeric characters, dashes, and + underscore characters; + + - map values can contain Unicode characters; + + - `search`: + + - `search` maximum length: 256 bytes; + + - map keys can contain alphanumeric characters, dashes, and + underscore characters; + + - list indexes must be integers (such as, `[0]` instead of + `["0"]`); + +- output: + + - custom resource response: 4,096 bytes maximum; this is a + CloudFormation quota - for more information, see _Custom + resource response_ in [Understand CloudFormation + quotas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html); + + - returned value can contain Unicode characters; + + - returned value type is represented as a Unicode string. + + +## Setup + +Install [rain](https://github.com/aws-cloudformation/rain), that +you'll use to create CloudFormation stacks to manage resources. When +ready, create the Lambda function, its execution role and log group, +in a given AWS account and AWS region (the example below uses +`us-east-1` as the AWS region; change this value as needed); such +resources will be backing the custom resource consumer: + +``` +rain deploy src/getfromjson.yml getfromjson \ + --region us-east-1 +``` + +Note that the `TagName` parameter for the `getfromjson.yml` template +is optional, and you can omit it if needed: its value defaults to +`GetFromJson`. `TagName` is used to add a tag called `Name`, with the +value for the `TagName` parameter, to resources that the template +describes (the Lambda function that backs relevant custom resources, +the function's execution role, and the function's log group). + + +## Usage + +Please make sure to follow the _Setup_ section above before +continuing. When ready, create a CloudFormation stack that uses an +example template showing you how to invoke the Lambda function (that +you created earlier) that backs up the custom resources you'll use to +extract values from example JSON input (extracted values will be +available in the `Outputs` section for the example stack you'll +create; note that you can also choose to consume such values from +properties of other resources you describe in the `Resources` section +of the template): + +``` +rain deploy example-templates/getfromjson-consumer.yml getfromjson-consumer \ + --region us-east-1 +``` + + +## Development + +Install the [AWS Serverless Application Model Command Line Interface +(AWS SAM CLI) +](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli.html) +in your workstation. When done, refer to the documentation on +[Installing Docker to use with the AWS SAM +CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-docker.html). + +Install [rain](https://github.com/aws-cloudformation/rain), that +you'll use to create CloudFormation stacks to manage, on your behalf, +the resources you'll need. + +Next, create and activate a [virtual +environment](https://docs.python.org/3/library/venv.html) for Python +using the following commands: + +``` +python -m venv venv +source venv/bin/activate +``` + +Next, install the following Python module(s) in your activated +environment: + +``` +python -m pip install --upgrade -r requirements-dev.txt +``` + +To run unit tests for `getfromjson` on your machine, use the following +command: + +``` +pytest --cov +``` + +To speed up the development lifecycle, you can locally invoke the +Lambda function code, and pass input events for your test use cases +(the unit tests for `getfromjson` do something similar as well, and by +invoking the Lambda function locally you add an integration testing +flavor to your development lifecycle). To do so, run the following +command from the root level of the project: + +``` +./run-local-invoke +``` + +The script above uses the SAM CLI to invoke the Lambda function code +locally on your machine; you'll need to have Docker installed and +running. The SAM CLI uses the content of the `template.yml` file in +the `src` directory to determine which settings to use for aspects +that include which `Runtime` to use, and the `MemorySize`: if you'll +need to adjust some of these values, make sure you reflect your +changes also in relevant parts of the `src/getfromjson.yml` +CloudFormation template; note that the value for `Handler` though +needs to have a different prefix depending on the file you use (it +should be `Handler: getfromjson.lambda_handler` in the SAM template, +and `Handler: index.lambda_handler` in the CloudFormation template). + +Note: the code for `getfromjson`, by default, uses the `INFO` logging +level - you'll need to update the following line and use a different +logging level (such as, `logging.DEBUG`) when developing and +troubleshooting the code): + +``` +LOGGER.setLevel(logging.INFO) +``` + +When ready to create the infrastructure for `getfromjson`, use the +following commands to do so (the example below uses `us-east-1` as the +AWS region; change this value as needed): + +``` +pylint src/ \ + && mypy \ + && pytest --cov \ + && bandit -c bandit.yaml -r src/ \ + && rain deploy src/getfromjson.yml getfromjson \ + --region us-east-1 +``` + +To create a stack with example custom resource consumers, run the +following command: + +``` +rain deploy example-templates/getfromjson-consumer.yml getfromjson-consumer \ + --region us-east-1 +``` diff --git a/CloudFormation/CustomResources/getfromjson/bandit.yaml b/CloudFormation/CustomResources/getfromjson/bandit.yaml new file mode 100644 index 00000000..02aa3f1f --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/bandit.yaml @@ -0,0 +1,3 @@ +# For more information, see https://bandit.readthedocs.io/en/latest/config.html + +exclude_dirs: ['tests'] diff --git a/CloudFormation/CustomResources/getfromjson/example-templates/getfromjson-consumer.yml b/CloudFormation/CustomResources/getfromjson/example-templates/getfromjson-consumer.yml new file mode 100644 index 00000000..e7fe05c2 --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/example-templates/getfromjson-consumer.yml @@ -0,0 +1,48 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: This AWS CloudFormation template describes a sample CloudFormation custom resource consumer for the GetFromJson Lambda-backed custom resource provider. + +Parameters: + GetFromListJsonData: + Description: Example JSON data representing a list of values. + Type: String + Default: '["test0", "test1", "test2"]' + + GetFromListJsonDataQuery: + Description: Example query for JSON data representing a list of values. + Type: String + Default: '[2]' + + GetFromMapJsonData: + Description: Example JSON data representing a map data structure. + Type: String + Default: '{"test": {"test1": ["x", "y"]}}' + + GetFromMapJsonDataQuery: + Description: Example query for JSON data representing a map data structure. + Type: String + Default: '["test"]["test1"][1]' + +Resources: + GetFromJsonCustomResourceSampleGetFromList: + Type: Custom::GetFromJson + Properties: + ServiceTimeout: 1 + ServiceToken: !ImportValue Custom-GetFromJson + json_data: !Ref GetFromListJsonData + search: !Ref GetFromListJsonDataQuery + + GetFromJsonCustomResourceSampleGetFromMap: + Type: Custom::GetFromJson + Properties: + ServiceTimeout: 1 + ServiceToken: !ImportValue Custom-GetFromJson + json_data: !Ref GetFromMapJsonData + search: !Ref GetFromMapJsonDataQuery + +Outputs: + GetFromJsonCustomResourceSampleGetFromListValue: + Value: !GetAtt GetFromJsonCustomResourceSampleGetFromList.Data + + GetFromJsonCustomResourceSampleGetFromMapValue: + Value: !GetAtt GetFromJsonCustomResourceSampleGetFromMap.Data diff --git a/CloudFormation/CustomResources/getfromjson/mypy.ini b/CloudFormation/CustomResources/getfromjson/mypy.ini new file mode 100644 index 00000000..897dcb73 --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +follow_imports = silent +strict = True +files = src/ diff --git a/CloudFormation/CustomResources/getfromjson/requirements-dev.txt b/CloudFormation/CustomResources/getfromjson/requirements-dev.txt new file mode 100644 index 00000000..40cfc9de --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/requirements-dev.txt @@ -0,0 +1,8 @@ +bandit>=1.7.9 +cfn-lint>=1.3.6 +cfnresponse>=1.1.4 +mypy>=1.10.1 +pip>=24.1 +pylint>=3.2.3 +pytest-cov>=5.0.0 +setuptools>=70.1.1 diff --git a/CloudFormation/CustomResources/getfromjson/run-local-invoke b/CloudFormation/CustomResources/getfromjson/run-local-invoke new file mode 100755 index 00000000..ed6a948c --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/run-local-invoke @@ -0,0 +1,44 @@ +#!/bin/bash + + +EVENTS_DIR='events' + +EVENT_FILES=( + 'event-consume-from-map.json' + 'event-consume-from-list.json' + 'event-consume-from-map-retrieval-error.json' + 'event-consume-from-list-retrieval-error.json' + 'event-empty-json-data-input.json' + 'event-empty-search-input.json' + 'event-invalid-json-data-input.json' + 'event-invalid-search-input.json' +) + + +run_local_invoke() { + echo "**** Invoking event from file: $event_file ****" + sam local invoke --event $EVENTS_DIR/$event_file + echo '-----------------------------' +} + + +cat < bool: + """Lambda function entry-point.""" + try: + json_data = event["ResourceProperties"]["json_data"] + search = event["ResourceProperties"]["search"] + + _validate_input( + json_data=json_data, + search=search, + ) + + if not IS_LOCAL_TESTING: + if event["RequestType"] == "Delete": + _send_response(event, context, cfnresponse.SUCCESS, {}) + # Return True only if the request type is "Delete". + return True + + response_data = {} + response_data["Data"] = _traverse( + data_from_json=json.loads(json_data), + search=search, + ) + LOGGER.debug(response_data) + + if not IS_LOCAL_TESTING: + _send_response(event, context, cfnresponse.SUCCESS, response_data) + + return True + except (IndexError, KeyError, ValueError) as invalid_input: + message = f"Error: {str(invalid_input)}" + LOGGER.error(message) + if not IS_LOCAL_TESTING: + _send_response(event, context, cfnresponse.FAILED, {}, message) + + return False + + +def _validate_input( + json_data: str, + search: str, +) -> None: + """Validate provided input.""" + if len(json_data.encode()) > JSON_DATA_MAX_BYTES: + raise ValueError( + f"json_data limit ({JSON_DATA_MAX_BYTES} bytes) exceeded." + ) + + if len(search.encode()) > SEARCH_MAX_BYTES: + raise ValueError(f"query limit ({SEARCH_MAX_BYTES} bytes) exceeded.") + + # Accepted input search values include: + # + # - bracket-enclosed input values represented as alphanumeric + # string keys (that might also contain dashes and + # underscores), like ["mytest"], whereas the string key itself + # is enclosed in double quotes and the trailing quote is not + # escaped, or + # + # - like the above, but with single quotes wrapping the value, + # like ['mytest'], or + # + # - integer values wrapped in brackets, like [0]. + # + # The following regular expression encompasses the three cases + # above, whereas each case is delimited by a logical OR ("|") + # character, and the check for the unescaped trailing quote is + # implemented with a negative lookbehind regex: "(? Any: + """Traverses the data structure, and returns the value matching search.""" + LOGGER.debug("data_from_json: %s", data_from_json) + + search_tokens = _get_search_tokens( + search=search, + ) + + # Used when removing quotes enclosing a token representing a map's + # key string: match map keys (alphanumeric characters, dashes, + # underscores) enclosed in quotes, whose trailing quote is not + # escaped. + search_token_map_key_pattern = "^((\"[a-zA-Z0-9_-]+(? List[Any]: + """Get tokens enclosed in brackets from search.""" + # Find all the tokens in square brackets from the search string; + # for example, '["test"]["test1"][1]' has 3 tokens: ["test"], + # ["test1"], and [1]. + search_tokens_pattern = "\\[(['\"a-zA-Z0-9_-]+)\\]" + LOGGER.debug("search_tokens_pattern: %s", search_tokens_pattern) + search_tokens = re.findall( + pattern=search_tokens_pattern, + string=search, + ) + LOGGER.debug("search tokens found: %s", search_tokens) + + return search_tokens + + +def _send_response( + event: Dict[str, Any], + context: Any, + response_status: str, + response_data: Dict[str, Any], + reason: Optional[str] = None, +) -> bool: + """Define a wrapper for send() in the cfnresponse module.""" + cfnresponse.send( + event, context, response_status, response_data, None, reason=reason + ) + + return True diff --git a/CloudFormation/CustomResources/getfromjson/src/getfromjson.yml b/CloudFormation/CustomResources/getfromjson/src/getfromjson.yml new file mode 100644 index 00000000..5eaa13e8 --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/src/getfromjson.yml @@ -0,0 +1,75 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: This template describes a Lambda function for a CloudFormation custom resource to consume and get a given value out of an input JSON data structure and an input search argument. + +Parameters: + TagName: + Description: Value for the Name tag. + Type: String + Default: GetFromJson + +Resources: + GetFromJsonLambdaFunction: + Type: AWS::Lambda::Function + DependsOn: GetFromJsonLogGroup + Metadata: + cfn-lint: + config: + ignore_checks: + - E3012 # Used for the `!Rain::Embed` directive below, whose `getfromjson.py` argument is deemed by cfn-lint not to be a string type. + Properties: + Code: + ZipFile: !Rain::Embed getfromjson.py + Description: Lambda function for a CloudFormation custom resource to consume and get a given value out of an input JSON data structure and an input search argument. + FunctionName: GetFromJson + Handler: index.lambda_handler + MemorySize: 128 + Role: !GetAtt GetFromJsonLambdaFunctionExecutionRole.Arn + Runtime: python3.12 + Tags: + - Key: Name + Value: !Ref TagName + Timeout: 10 + + GetFromJsonLambdaFunctionExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Version: "2012-10-17" + Path: / + Policies: + - PolicyDocument: + Statement: + - Action: + - logs:CreateLogStream + - logs:PutLogEvents + Effect: Allow + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/GetFromJson* + Version: "2012-10-17" + PolicyName: GetFromJsonLambdaFunctionExecutionRole + RoleName: !Sub GetFromJson-${AWS::Region} + Tags: + - Key: Name + Value: !Ref TagName + + GetFromJsonLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /aws/lambda/GetFromJson + RetentionInDays: 3653 + Tags: + - Key: Name + Value: !Ref TagName + +Outputs: + GetFromJsonLambdaFunctionArn: + Value: !GetAtt GetFromJsonLambdaFunction.Arn + Export: + Name: Custom-GetFromJson diff --git a/CloudFormation/CustomResources/getfromjson/src/template.yml b/CloudFormation/CustomResources/getfromjson/src/template.yml new file mode 100644 index 00000000..6ece0e3c --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/src/template.yml @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: AWS SAM template for testing the getfromjson module on your machine. + +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + MemorySize: 128 + Timeout: 10 + +Resources: + GetFromJsonLambdaFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: getfromjson.lambda_handler + Runtime: python3.12 diff --git a/CloudFormation/CustomResources/getfromjson/src/tests/__init__.py b/CloudFormation/CustomResources/getfromjson/src/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CloudFormation/CustomResources/getfromjson/src/tests/test_getfromjson.py b/CloudFormation/CustomResources/getfromjson/src/tests/test_getfromjson.py new file mode 100644 index 00000000..58e0a85a --- /dev/null +++ b/CloudFormation/CustomResources/getfromjson/src/tests/test_getfromjson.py @@ -0,0 +1,342 @@ +"""Tests for the getfromjson.py module.""" + +from typing import ( + Any, + Dict, +) +from unittest.mock import patch + +import cfnresponse # type: ignore +from pytest import raises + +from .. import getfromjson + + +def test_given_json_list_when_traversed_with_invalid_search_then_it_should_raise_exception() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + data_from_json = ["test0", "test1", "test2"] + search = "[3]" + + with raises(IndexError): + getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + +def test_given_json_list_when_traversed_with_valid_search_then_it_should_return_valid_value() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + data_from_json = ["test0", "test1", "test2"] + search = "[2]" + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "test2" + + +def test_given_json_map_when_traversed_with_invalid_search_then_it_should_raise_exception() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + data_from_json = {"test": {"test-1": ["x", "y"]}} + search = '["test"]["test-2"][1]' + + with raises(KeyError): + getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + +def test_given_json_map_when_traversed_with_valid_search_then_it_should_return_valid_value() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + data_from_json = {"test": {"test-1": ["x", "y"]}} + search = '["test"]["test-1"][1]' + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "y" + + +def test_given_json_map_with_allowed_index_keys_when_traversed_with_valid_search_then_it_should_return_valid_value() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + data_from_json = {"testTest012-_": {"test-1": ["x", "y"]}} + search = '["testTest012-_"]["test-1"][1]' + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "y" + + +def test_given_json_list_with_unicode_values_when_traversed_with_valid_search_then_it_should_return_expected_values() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + data_from_json = ["√√ test0", "√√ test1", "√√ test2"] + search = "[2]" + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "√√ test2" + + +def test_given_json_map_with_unicode_values_when_traversed_with_valid_search_then_it_should_return_expected_values() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + data_from_json = {"testTest012-_": {"test-1": ["x", "√√ test"]}} + search = '["testTest012-_"]["test-1"][1]' + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "√√ test" + + +def test_given_search_with_single_quotes_when_traversing_map_then_it_should_return_expected_values() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + data_from_json = {"testTest012-_": {"test-1": ["x", "y"]}} + search = "['testTest012-_']['test-1'][1]" + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "y" + + +def test_given_search_with_alternate_single_and_double_quotes_across_tokens_when_traversing_map_then_it_should_return_expected_values() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + data_from_json = {"testTest012-_": {"test-1": ["x", "y"]}} + search = "['testTest012-_'][\"test-1\"][1]" + + value = getfromjson._traverse( # pylint: disable=W0212 + data_from_json=data_from_json, + search=search, + ) + + assert value == "y" + + +def test_given_json_data_max_bytes_exceeded_when_consumed_then_request_should_return_false() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + json_data_max_bytes_current = getfromjson.JSON_DATA_MAX_BYTES + getfromjson.JSON_DATA_MAX_BYTES = 1 + + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = True + + json_data = '["test0", "test1", "test2"]' + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + + assert getfromjson.lambda_handler(event, None) is False + + getfromjson.JSON_DATA_MAX_BYTES = json_data_max_bytes_current + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_search_max_bytes_exceeded_when_consumed_then_request_should_return_false() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + search_max_bytes_current = getfromjson.SEARCH_MAX_BYTES + getfromjson.SEARCH_MAX_BYTES = 1 + + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = True + + json_data = '["test0", "test1", "test2"]' + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + + assert getfromjson.lambda_handler(event, None) is False + + getfromjson.SEARCH_MAX_BYTES = search_max_bytes_current + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_invalid_json_data_when_consumed_then_request_should_return_false() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = True + + json_data = "invalid" + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + + assert getfromjson.lambda_handler(event, None) is False + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_invalid_search_when_consumed_then_request_should_return_false() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = True + + json_data = '["test0", "test1", "test2"]' + search = "invalid" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + + assert getfromjson.lambda_handler(event, None) is False + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_valid_json_data_and_search_when_consumed_then_request_should_return_true() -> ( # noqa: D103 E501 # pylint: disable=C0116 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = True + + json_data = '["test0", "test1", "test2"]' + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + + assert getfromjson.lambda_handler(event, None) is True + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_delete_request_type_when_consumed_then_cfnresponse_should_be_called_with_success_response() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = False + + json_data = '["test0", "test1", "test2"]' + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + event["RequestType"] = "Delete" + + with patch( + "src.getfromjson._send_response", + return_value=None, + ): + assert getfromjson.lambda_handler(event, None) is True + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_non_delete_request_type_when_valid_input_is_consumed_then_cfnresponse_should_be_called_with_success_response() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = False + + json_data = '["test0", "test1", "test2"]' + search = "[2]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + event["RequestType"] = "Create" + + with patch( + "src.getfromjson._send_response", + return_value=None, + ): + assert getfromjson.lambda_handler(event, None) is True + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_non_delete_request_type_when_invalid_input_is_consumed_then_cfnresponse_should_be_called_with_failed_response() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + is_local_testing_current = getfromjson.IS_LOCAL_TESTING + getfromjson.IS_LOCAL_TESTING = False + + json_data = '["test0", "test1", "test2"]' + search = "[3]" + + event: Dict[str, Any] = {} + event["ResourceProperties"] = {} + event["ResourceProperties"]["json_data"] = json_data + event["ResourceProperties"]["search"] = search + event["RequestType"] = "Create" + + with patch( + "src.getfromjson._send_response", + return_value=None, + ): + assert getfromjson.lambda_handler(event, None) is False + + getfromjson.IS_LOCAL_TESTING = is_local_testing_current + + +def test_given_response_to_send_when_sent_with_cfnresponse_then_it_should_return_true() -> ( # noqa: D103 E501 # pylint: disable=C0116,C0301 + None +): + class Context: # pylint: disable=R0903 + """Mock Context class.""" + + def __init__(self) -> None: + self.log_stream_name = "test" + + event = {} + event["ResponseURL"] = "https://mock_value" + event["StackId"] = "mock_value" + event["RequestId"] = "mock_value" + event["LogicalResourceId"] = "mock_value" + + with patch( + "cfnresponse.send", + return_value=None, + ): + value = getfromjson._send_response( # pylint: disable=W0212 + event, + Context(), + cfnresponse.SUCCESS, # noqa: E501 # pylint: disable=E0606 + {}, + ) + + assert value is True