# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from tests import unittest from botocore.translate import ModelFiles, translate, merge_dicts, \ resembles_jmespath_exp SERVICES = { "service_full_name": "AWS Security Token Service", "service_abbreviation": "AWS STS", "type": "query", "signature_version": "v4", "result_wrapped": True, "global_endpoint": "sts.amazonaws.com", "api_version": "2011-06-15", "endpoint_prefix": "sts", "xmlnamespace": "https://sts.amazonaws.com/doc/2011-06-15/", "documentation": "docs", "operations": { "AssumeRole": { "name": "AssumeRole", "input": { "shape_name": "AssumeRoleRequest", "type": "structure", "members": { "RoleArn": { "shape_name": "arnType", "type": "string", "min_length": 20, "max_length": 2048, "documentation": "docs", "required": True }, "RoleSessionName": { "shape_name": "userNameType", "type": "string", "min_length": 2, "max_length": 32, "pattern": "[\\w+=,.@-]*", "documentation": "docs", "required": True }, "Policy": { "shape_name": "sessionPolicyDocumentType", "type": "string", "pattern": "[\\u0009\\u000A\\u000D\\u0020-\\u00FF]+", "min_length": 1, "max_length": 2048, "documentation": "docs" }, "DurationSeconds": { "shape_name": "roleDurationSecondsType", "type": "integer", "min_length": 900, "max_length": 3600, "documentation": "docs" }, "ExternalId": { "shape_name": "externalIdType", "type": "string", "min_length": 2, "max_length": 96, "pattern": "[\\w+=,.@:-]*", "documentation": "docs" }, # Not in the actual description, but this is to test pagination. "NextToken": { "shape_name": "String", "type": "string", "documentation": None }, "TokenToken": { "shape_name": "String", "type": "string", "documentation": None, "xmlname": "tokenToken" }, "MaxResults": { "shape_name": "Integer", "type": "int", "documentation": None, "xmlname": "maxResults" } }, "documentation": "docs" }, "output": { "shape_name": "AssumeRoleResponse", "type": "structure", "members": { "Credentials": { "shape_name": "Credentials", "type": "structure", "members": { "AccessKeyId": { "shape_name": "accessKeyIdType", "type": "string", "min_length": 16, "max_length": 32, "pattern": "[\\w]*", "documentation": "docs", "required": True }, "SecretAccessKey": { "shape_name": "accessKeySecretType", "type": "string", "documentation": "docs", "required": True }, "SessionToken": { "shape_name": "tokenType", "type": "string", "documentation": "docs", "required": True }, "Expiration": { "shape_name": "dateType", "type": "timestamp", "documentation": "docs", "required": True } }, "documentation": "docs" }, "AssumedRoleUser": { "shape_name": "AssumedRoleUser", "type": "structure", "members": { "AssumedRoleId": { "shape_name": "assumedRoleIdType", "type": "string", "min_length": 2, "max_length": 96, "pattern": "[\\w+=,.@:-]*", "documentation": "docs", "required": True }, "Arn": { "shape_name": "arnType", "type": "string", "min_length": 20, "max_length": 2048, "documentation": "docs", "required": True } }, "documentation": "docs" }, "PackedPolicySize": { "shape_name": "nonNegativeIntegerType", "type": "integer", "min_length": 0, "documentation": "docs" }, "NextToken": { "shape_name": "String", "type": "string", "documentation": None, "xmlname": "nextToken" }, }, "documentation": "docs" }, "errors": [ { "shape_name": "MalformedPolicyDocumentException", "type": "structure", "members": { "message": { "shape_name": "malformedPolicyDocumentMessage", "type": "string", "documentation": "docs" } }, "documentation": "docs" }, { "shape_name": "PackedPolicyTooLargeException", "type": "structure", "members": { "message": { "shape_name": "packedPolicyTooLargeMessage", "type": "string", "documentation": "docs" } }, "documentation": "docs" } ], "documentation": "docs" }, "RealOperation2013_02_04": { "name": "RealOperation2013_02_04", "input": {}, "output": { "shape_name": "RealOperation2013_02_04Response", "type": "structure", "members": { "Result": { "shape_name": "Result", "type": "string", "documentation": "" } } }, "errors": [], "documentation": "docs" }, "NoOutputOperation": { "name": "NoOutputOperation", "input": {}, "output": {}, "errors": [], "documentation": "docs" }, "DeprecatedOperation": { "input": { "shape_name": "DeprecatedOperationRequest", "type": "structure", "members": { "FooBar": { "shape_name": "foobarType", "type": "string", "documentation": "blah blah blah blah", }, "FieBaz": { "shape_name": "fiebazType", "type": "string", "documentation": "Don't use this, it's deprecated" } } }, "documentation": "This is my stuff" }, "DeprecatedOperation2": { "input": { "shape_name": "DeprecatedOperation2Request", "type": "structure", "members": { "FooBar": { "shape_name": "foobarType", "type": "string", "documentation": "blah blah blah blah", }, "FieBaz": { "shape_name": "fiebazType", "type": "string", "documentation": "" } } }, "documentation": "This operation has been deprecated." }, "RenameOperation": { "input": { "shape_name": "RenameOperation", "type": "structure", "members": { "RenameMe": { "shape_name": "RenameMe", "type": "string", "documentation": "blah blah blah blah", }, "FieBaz": { "shape_name": "fiebazType", "type": "string", "documentation": "" } } }, "documentation": "This operation has been deprecated." }, "EchoedOutputParams": { "name": "EchoedOutputParams", "input": { "shape_name": "EchoedOutputParams", "type": "structure", "members": { "Marker": { "shape_name": "String", "type": "string", "documentation": "blah blah blah blah", }, } }, "documentation": "", "output": { "shape_name": "EchoedOutputParamsResponse", "type": "structure", "members": { "Marker": { "shape_name": "String", "type": "string", "documentation": "", }, "NextMarker": { "shape_name": "String", "type": "string", "documentation": "", }, "ResultKey": { "shape_name": "String", "type": "string", "documentation": "", }, } }, } } } class TestTranslateExtensions(unittest.TestCase): def setUp(self): self.model = ModelFiles(SERVICES, {}, {}, {}) def test_can_add_extras_top_level_keys(self): # A use case here would be adding the iterator/waiter configs. new_keys = {'extra': {'paginators': 'paginator_config'}} self.model.enhancements = new_keys new_model = translate(self.model) # There should be a new top level key 'iterators' that was merged in. self.assertEqual(new_model['paginators'], 'paginator_config') self.assertIn('operations', new_model) def test_can_add_fields_to_operation(self): # A use case would be to add checksum info for a param. # We could go for a more streamlined syntax, but this way, it's # clear how this maps onto the existing json model. new_keys = { 'operations': { 'AssumeRole': { 'input': { 'checksum': 'md5', } }, } } self.model.enhancements = new_keys new_model = translate(self.model) self.assertEqual( new_model['operations']['AssumeRole']['input']['checksum'], 'md5') def test_can_add_fields_to_op_params(self): # A use case would be if we want to annotate that a # string type might also come from a file (keypairs, s3 uploads, etc). new_keys = { 'operations': { 'AssumeRole': { 'input': { 'members': { 'Policy': { 'alsofrom': 'filename', }, } } }, } } self.model.enhancements = new_keys new_model = translate(self.model) self.assertEqual( new_model['operations']['AssumeRole']['input']['members']\ ['Policy']['alsofrom'], 'filename') self.assertEqual( new_model['operations']['AssumeRole']['input']['members']\ ['Policy']['type'], 'string') self.assertEqual( new_model['operations']['AssumeRole']['input']['members']\ ['RoleArn']['shape_name'], 'arnType') class TestTranslateModel(unittest.TestCase): def setUp(self): self.model = ModelFiles(SERVICES, {}, {}, {}) def test_operations_is_a_dict_not_list(self): # In order to make overriding easier, we want the list of # operations to be a dict, not a list. The way we don't have # to search through the list to find the operation we want to # change. It also makes it easier to annontate operations. new_model = translate(self.model) self.assertIn('AssumeRole', new_model['operations']) def test_iterators_are_merged_into_operations(self): # This may or may not pan out, but if a pagination config is # specified, that info is merged into the specific operations. extra = { 'pagination': { 'AssumeRole': { 'input_token': 'NextToken', 'output_token': 'NextToken', 'limit_key': 'MaxResults', 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], } } } self.model.enhancements = extra new_model = translate(self.model) op = new_model['operations']['AssumeRole'] self.assertDictEqual( op['pagination'], { 'input_token': 'NextToken', 'py_input_token': 'next_token', 'output_token': 'NextToken', 'limit_key': 'MaxResults', 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], }) def test_py_input_name_is_not_added_if_it_exists(self): # This may or may not pan out, but if a pagination config is # specified, that info is merged into the specific operations. extra = { 'pagination': { 'AssumeRole': { 'input_token': 'NextToken', 'output_token': 'NextToken', 'py_input_token': 'other_value', 'limit_key': 'MaxResults', 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], } } } self.model.enhancements = extra new_model = translate(self.model) op = new_model['operations']['AssumeRole'] # Note how 'py_input_token' is left untouched. This allows us # to override this naming if we need to. self.assertDictEqual( op['pagination'], { 'input_token': 'NextToken', 'py_input_token': 'other_value', 'output_token': 'NextToken', 'limit_key': 'MaxResults', 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], }) def test_paginators_are_validated(self): # Can't create a paginator config that refers to a non existent # operation. extra = { 'pagination': { 'UnknownOperation': { 'input_token': 'NextToken', 'output_token': 'NextToken', 'max_results': 'MaxResults', } } } self.model.enhancements = extra with self.assertRaises(ValueError): new_model = translate(self.model) def test_paginators_are_placed_into_top_level_key(self): extra = { 'pagination': { 'AssumeRole': { 'input_token': 'NextToken', 'output_token': 'NextToken', 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], } } } self.model.enhancements = extra new_model = translate(self.model) self.assertEqual(new_model['pagination'], extra['pagination']) def test_extra_key(self): # Anything in "extra" is merged as a top level key. extra = { "extra": { "signature_version": "v2", } } self.model.enhancements = extra new_model = translate(self.model) self.assertEqual(new_model['signature_version'], 'v2') self.assertEqual(new_model['documentation'], 'docs') def test_paginator_with_multiple_input_outputs(self): extra = { 'pagination': { 'AssumeRole': { 'input_token': ['NextToken', 'TokenToken'], 'output_token': ['NextToken'], 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], } } } self.model.enhancements = extra new_model = translate(self.model) op = new_model['operations']['AssumeRole'] self.assertDictEqual( op['pagination'], { 'input_token': ['NextToken', 'TokenToken'], 'py_input_token': ['next_token', 'token_token'], 'output_token': ['NextToken'], 'result_key': 'Credentials', 'non_aggregate_keys': ['PackedPolicySize', 'AssumedRoleUser'], }) def test_result_key_validation(self): # result_key must exist. extra = { 'pagination': { 'AssumeRole': { 'input_token': ['Token', 'TokenToken'], 'output_token': ['NextToken'] } } } self.model.enhancements = extra with self.assertRaises(ValueError): translate(self.model) def test_result_key_exists_in_output(self): extra = { 'pagination': { 'AssumeRole': { 'input_token': ['Token', 'TokenToken'], 'output_token': ['NextToken', 'NextTokenToken'], 'result_key': 'DoesNotExist', } } } self.model.enhancements = extra with self.assertRaises(ValueError): translate(self.model) def test_result_key_can_be_a_list(self): extra = { 'pagination': { 'AssumeRole': { 'input_token': ['NextToken'], 'output_token': ['NextToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], 'non_aggregate_keys': ['PackedPolicySize'], } } } self.model.enhancements = extra new_model = translate(self.model) self.assertEqual(new_model['pagination'], extra['pagination']) def test_expected_schema_exists(self): # In this case, the key 'output_tokens' is suppose to be 'output_token' # so we should get an error when this happens. extra = { 'pagination': { 'AssumeRole': { 'input_token': ['Token', 'TokenToken'], 'output_tokens': ['NextToken', 'NextTokenToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], } } } self.model.enhancements = extra with self.assertRaises(ValueError): translate(self.model) def test_input_tokens_exist_in_model(self): extra = { 'pagination': { 'AssumeRole': { # In this case, "DoesNotExist" token is not in the input # model, so we get an exception complaining about this. 'input_token': ['NextToken', 'DoesNotExist'], 'output_token': ['NextToken', 'NextTokenToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], } } } self.model.enhancements = extra with self.assertRaises(ValueError): translate(self.model) def test_validate_limit_key_is_in_input(self): extra = { 'pagination': { 'AssumeRole': { 'input_token': 'NextToken', 'output_token': ['NextToken', 'NextTokenToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], # In this case, "DoesNotExist" token is not in the input # model, so we get an exception complaining about this. 'limit_key': 'DoesNotExist', } } } self.model.enhancements = extra with self.assertRaises(ValueError): translate(self.model) def test_cant_add_pagination_to_nonexistent_operation(self): extra = { 'pagination': { 'ThisOperationDoesNotExist': { 'input_token': 'NextToken', 'output_token': ['NextToken', 'NextTokenToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], 'limit_key': 'Foo', } } } self.model.enhancements = extra with self.assertRaisesRegexp( ValueError, "Trying to add pagination config for non " "existent operation: ThisOperationDoesNotExist"): translate(self.model) def test_skip_jmespath_validation(self): # This would fail previously. extra = { 'pagination': { 'AssumeRole': { 'input_token': ['NextToken'], 'output_token': ['NextToken'], 'result_key': 'Credentials.AssumedRoleUser', } } } self.model.enhancements = extra new_model = translate(self.model) self.assertEqual(new_model['pagination'], extra['pagination']) def test_result_key_validation_with_no_output(self): extra = { 'pagination': { # RealOperation does not have any output members so # we should get an error message telling us this. 'NoOutputOperation': { 'input_token': 'NextToken', 'output_token': ['NextToken', 'NextTokenToken'], 'result_key': ['Credentials', 'AssumedRoleUser'], 'limit_key': 'Foo', } } } self.model.enhancements = extra with self.assertRaisesRegexp( ValueError, "Trying to add pagination config for an " "operation with no output members: " "NoOutputOperation"): translate(self.model) def test_echoed_input_params_ignored(self): extra = { 'pagination': { 'EchoedOutputParams': { 'input_token': ['Marker'], 'output_token': ['NextMarker'], 'result_key': 'ResultKey', } } } self.model.enhancements = extra new_model = translate(self.model) self.assertEqual(new_model['pagination'], extra['pagination']) class TestBuildRetryConfig(unittest.TestCase): def setUp(self): self.retry = { "definitions": { "def_name": { "from": {"definition": "file"} } }, "retry": { "__default__": { "max_attempts": 5, "delay": "global_delay", "policies": { "global_one": "global", "override_me": "global", } }, "sts": { "__default__": { "delay": "service_specific_delay", "policies": { "service_one": "service", "override_me": "service", } }, "AssumeRole": { "policies": { "name": "policy", "other": {"$ref": "def_name"} } } } } } def test_inject_retry_config(self): model = ModelFiles(SERVICES, self.retry, {}) new_model = translate(model) self.assertIn('retry', new_model) retry = new_model['retry'] self.assertIn('__default__', retry) self.assertEqual( retry['__default__'], { "max_attempts": 5, "delay": "service_specific_delay", "policies": { "global_one": "global", "override_me": "service", "service_one": "service", } } ) # Policies should be merged. operation_config = retry['AssumeRole'] self.assertEqual(operation_config['policies']['name'], 'policy') def test_resolve_reference(self): model = ModelFiles(SERVICES, self.retry, {}) new_model = translate(model) operation_config = new_model['retry']['AssumeRole'] # And we should resolve references. self.assertEqual(operation_config['policies']['other'], {"from": {"definition": "file"}}) class TestReplacePartOfOperation(unittest.TestCase): def test_replace_operation_key_name(self): enhancements = { 'transformations': { 'operation-name': {'remove': r'\d{4}_\d{2}_\d{2}'} } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) new_model = translate(model) # But the key into the operation dict is stripped of the # matched regex. self.assertEqual(list(sorted(new_model['operations'].keys())), ['AssumeRole', 'DeprecatedOperation', 'DeprecatedOperation2', 'EchoedOutputParams', 'NoOutputOperation','RealOperation', 'RenameOperation']) # But the name key attribute is left unchanged. self.assertEqual(new_model['operations']['RealOperation']['name'], 'RealOperation2013_02_04') def test_merging_occurs_after_transformation(self): enhancements = { 'transformations': { 'operation-name': {'remove': r'\d{4}_\d{2}_\d{2}'} }, 'operations': { 'RealOperation': { 'input': { 'checksum': 'md5', } }, } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) model.enhancements = enhancements new_model = translate(model) self.assertIn('RealOperation', new_model['operations']) self.assertEqual( new_model['operations']['RealOperation']['input']['checksum'], 'md5') class TestRemovalOfDeprecatedParams(unittest.TestCase): def test_remove_deprecated_params(self): enhancements = { 'transformations': { 'remove-deprecated-params': {'deprecated_keyword': 'deprecated'} } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) new_model = translate(model) operation = new_model['operations']['DeprecatedOperation'] # The deprecated param should be gone, the other should remain self.assertIn('FooBar', operation['input']['members']) self.assertNotIn('FieBaz', operation['input']['members']) class TestRemovalOfDeprecatedOps(unittest.TestCase): def test_remove_deprecated_ops(self): enhancements = { 'transformations': { 'remove-deprecated-operations': {'deprecated_keyword': 'deprecated'} } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) new_model = translate(model) # The deprecated operation should be gone self.assertNotIn('DeprecatedOperation2', new_model['operations']) class TestFilteringOfDocumentation(unittest.TestCase): def test_remove_deprecated_params(self): enhancements = { "transformations": { "filter-documentation": { "filter": { "regex": "", "replacement": "" } } } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) new_model = translate(model) operation = new_model['operations']['DeprecatedOperation'] # The deprecated param should be gone, the other should remain self.assertEqual(operation['documentation'], 'This is my stuff') param = operation['input']['members']['FooBar'] self.assertEqual(param['documentation'], 'blah blah blah blah') class TestRenameParams(unittest.TestCase): def test_rename_param(self): enhancements = { 'transformations': { 'renames': { 'RenameOperation.input.members.RenameMe': 'BeenRenamed', } } } model = ModelFiles(SERVICES, retry={}, enhancements=enhancements) new_model = translate(model) arguments = new_model['operations']['RenameOperation']['input']['members'] self.assertNotIn('RenameMe', arguments) self.assertIn('BeenRenamed', arguments) class TestWaiterDenormalization(unittest.TestCase): maxDiff = None def setUp(self): self.model = ModelFiles(SERVICES, {}, {}) def test_waiter_default_resolved(self): extra = { 'waiters': { '__default__': { 'interval': 20, 'operation': 'AssumeRole', 'max_attempts': 25, 'acceptor_type': 'output', 'acceptor_path': 'path', 'acceptor_value': 'value', }, # Note that this config doesn't make any actual sense, # this is just testing we denormalize fields properly. 'RoleExists': { 'operation': 'AssumeRole', 'ignore_errors': ['Error1'], 'success_type': 'output', 'success_path': 'Table.TableStatus', 'success_value': ['ACTIVE'], } } } self.model.enhancements = extra new_model = translate(self.model) denormalized = { 'RoleExists': { 'interval': 20, 'max_attempts': 25, 'operation': 'AssumeRole', 'ignore_errors': ['Error1'], 'success': { 'type': 'output', 'path': 'Table.TableStatus', 'value': ['ACTIVE'], }, 'failure': { 'type': 'output', 'path': 'path', 'value': ['value'], } } } self.assertEqual(new_model['waiters'], denormalized) def test_default_and_extends(self): extra = { 'waiters': { '__default__': { 'interval': 20, 'max_attempts': 25, }, '__RoleResource': { 'operation': 'AssumeRole', 'max_attempts': 50, }, 'RoleExists': { 'extends': '__RoleResource', 'ignore_errors': ['Error1'], 'success_type': 'output', 'success_path': 'Table.TableStatus', 'success_value': 'ACTIVE', 'failure_type': 'output', 'failure_path': 'Table.TableStatus', 'failure_value': 'ACTIVE', } } } self.model.enhancements = extra new_model = translate(self.model) denormalized = { 'RoleExists': { # From __default__ 'interval': 20, # Overriden from __RoleResource 'max_attempts': 50, # Defined in __RoleResource 'operation': 'AssumeRole', # Defined in RoleExists 'ignore_errors': ['Error1'], 'success': { 'type': 'output', 'path': 'Table.TableStatus', 'value': ['ACTIVE'], }, 'failure': { 'type': 'output', 'path': 'Table.TableStatus', 'value': ['ACTIVE'], } } } self.assertEqual(new_model['waiters'], denormalized) def test_acceptor_path_resolution(self): extra = { 'waiters': { 'RoleExists': { 'operation': 'AssumeRole', 'acceptor_type': 'output', 'acceptor_path': 'acceptor_path', 'success_value': 'success_value', 'failure_value': 'failure_value', } } } self.model.enhancements = extra new_model = translate(self.model) denormalized = { 'RoleExists': { 'operation': 'AssumeRole', # We should only have success/failure values, # no acceptor types, those are all resolved. 'success': { # From acceptor_type. 'type': 'output', # From acceptor_path. 'path': 'acceptor_path', # From success_value. 'value': ['success_value'], }, 'failure': { # From acceptor_type. 'type': 'output', # From acceptor_path. 'path': 'acceptor_path', # From failure_value. 'value': ['failure_value'], } } } self.assertEqual(new_model['waiters'], denormalized) def test_only_acceptors_provided(self): extra = { 'waiters': { 'RoleExists': { 'operation': 'AssumeRole', 'acceptor_type': 'output', 'acceptor_path': 'acceptor_path', 'acceptor_value': 'acceptor_value', } } } self.model.enhancements = extra new_model = translate(self.model) denormalized = { 'RoleExists': { 'operation': 'AssumeRole', 'success': { 'type': 'output', 'path': 'acceptor_path', 'value': ['acceptor_value'], }, 'failure': { 'type': 'output', 'path': 'acceptor_path', 'value': ['acceptor_value'], } } } self.assertEqual(new_model['waiters'], denormalized) def test_validate_valid_operation(self): extra = { 'waiters': { '__default__': { 'interval': 20, 'max_attempts': 25, }, '__RoleResource': { 'operation': 'THISOPERATIONDOESNOTEXIST', 'max_attempts': 50, }, 'RoleExists': { 'extends': '__RoleResource', 'ignore_errors': ['Error1'], 'success_type': 'output', 'success_path': 'Table.TableStatus', 'success_value': 'ACTIVE', 'failure_type': 'output', 'failure_path': 'Table.TableStatus', 'failure_value': 'ACTIVE', } } } self.model.enhancements = extra with self.assertRaises(ValueError): new_model = translate(self.model) def test_only_success_defined(self): extra = { 'waiters': { '__default__': { "interval": 15, "max_attempts": 40, "acceptor_type": "output" }, 'AssumeRole': { "operation": "AssumeRole", "success_path": "Snapshots[].State", "success_value": "completed" } } } self.model.enhancements = extra new_model = translate(self.model) denormalized = { 'AssumeRole': { 'interval': 15, 'max_attempts': 40, 'operation': 'AssumeRole', 'success': { 'type': 'output', 'path': 'Snapshots[].State', 'value': ['completed'], }, # This is technically "incorrect", in the sense that this is # not a valid waiter config, but the waiter module has tests # to verify that it handles this "incorrect" case. 'failure': { 'type': 'output', } } } self.assertEqual(new_model['waiters'], denormalized) class TestResemblesJMESPath(unittest.TestCase): maxDiff = None def test_is_jmespath(self): self.assertTrue(resembles_jmespath_exp('Something.Else')) self.assertTrue(resembles_jmespath_exp('Something[1]')) def test_is_not_jmespath(self): self.assertFalse(resembles_jmespath_exp('Something')) if __name__ == '__main__': unittest.main()