Skip to content

Commit

Permalink
Add property transform for contract tests
Browse files Browse the repository at this point in the history
  • Loading branch information
zjinmei committed Oct 28, 2021
1 parent 0023563 commit e6cadc4
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 12 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pytest-cov>=2.7.1
pytest-random-order>=1.0.4
hypothesis>=4.32.3
pytest-localserver>=0.5.0
pyjq==2.5.2

# commit hooks
pre-commit>=1.18.1
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ include_trailing_comma = true
combine_as_imports = True
force_grid_wrap = 0
known_first_party = rpdk
known_third_party = boto3,botocore,cfn_tools,cfnlint,colorama,docker,hypothesis,jinja2,jsonschema,nested_lookup,ordered_set,pkg_resources,pytest,pytest_localserver,setuptools,yaml
known_third_party = boto3,botocore,cfn_tools,cfnlint,colorama,docker,hypothesis,jinja2,jsonschema,nested_lookup,ordered_set,pkg_resources,pyjq,pytest,pytest_localserver,setuptools,yaml

[tool:pytest]
# can't do anything about 3rd part modules, so don't spam us
Expand Down
52 changes: 46 additions & 6 deletions src/rpdk/core/contract/resource_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from uuid import uuid4

import docker
import pyjq
from botocore import UNSIGNED
from botocore.config import Config

Expand Down Expand Up @@ -225,13 +226,33 @@ def _update_schema(self, schema):
self.write_only_paths = self._properties_to_paths("writeOnlyProperties")
self.create_only_paths = self._properties_to_paths("createOnlyProperties")
self.properties_without_insertion_order = self.get_metadata()
self.property_transform_keys = self._properties_to_paths("propertyTransform")
self.property_transform = self._schema.get("propertyTransform")

additional_identifiers = self._schema.get("additionalIdentifiers", [])
self._additional_identifiers_paths = [
{fragment_decode(prop, prefix="") for prop in identifier}
for identifier in additional_identifiers
]

def transform_model(self, input_model):
if not self.property_transform:
return None
for key in self.property_transform_keys:
path = "/" + "/".join(key)
expression = self.property_transform[path]
tranformed_value = pyjq.first(expression, input_model)
input_model = self.update_property(input_model, tranformed_value, key[1:])

return input_model

def update_property(self, model, value, path):
if len(path) > 1:
model = self.update_property(model[path[0]], value, path[1:])
elif len(path) == 1:
model[path[0]] = value
return model

def has_only_writable_identifiers(self):
return all(
path in self.create_only_paths for path in self.primary_identifier_paths
Expand Down Expand Up @@ -395,7 +416,7 @@ def generate_invalid_update_example(self, create_model):
example = override_properties(self.invalid_strategy.example(), overrides)
return {**create_model, **example}

def compare(self, inputs, outputs, path=()):
def compare(self, inputs, outputs, transformed_inputs, path=()):
assertion_error_message = (
"All properties specified in the request MUST "
"be present in the model returned, and they MUST"
Expand All @@ -407,27 +428,46 @@ def compare(self, inputs, outputs, path=()):
for key in inputs:
new_path = path + (key,)
if isinstance(inputs[key], dict):
self.compare(inputs[key], outputs[key], new_path)
self.compare(
inputs[key],
outputs[key],
transformed_inputs[key] if transformed_inputs else None,
new_path,
)
elif isinstance(inputs[key], list):
assert len(inputs[key]) == len(outputs[key])

is_ordered = traverse_raw_schema(self._schema, new_path).get(
"insertionOrder", True
)
self.compare_collection(
inputs[key], outputs[key], is_ordered, new_path
inputs[key],
outputs[key],
is_ordered,
new_path,
transformed_inputs[key] if transformed_inputs else None,
)
else:
assert inputs[key] == outputs[key], assertion_error_message
else:
assert inputs == outputs, assertion_error_message
except Exception as exception:
raise AssertionError(assertion_error_message) from exception
# When input model not equal to output model, and there is transformed model,
# need to compare transformed model with output model also
if transformed_inputs:
self.compare(transformed_inputs, outputs, None)
else:
raise AssertionError(assertion_error_message) from exception

def compare_collection(self, inputs, outputs, is_ordered, path):
def compare_collection(self, inputs, outputs, is_ordered, path, transformed_inputs):
if is_ordered:
for index in range(len(inputs)): # pylint: disable=C0200
self.compare(inputs[index], outputs[index], path)
self.compare(
inputs[index],
outputs[index],
transformed_inputs[index] if transformed_inputs else None,
path,
)
return

assert {item_hash(item) for item in inputs} == {
Expand Down
6 changes: 6 additions & 0 deletions src/rpdk/core/contract/suite/handler_commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,13 @@ def test_input_equals_output(resource_client, input_model, output_model):
pruned_output_model = prune_properties_if_not_exist_in_path(
pruned_output_model, pruned_input_model, resource_client.create_only_paths
)

transformed_input_model = resource_client.transform_model(
copy.deepcopy(pruned_input_model)
)

resource_client.compare(
pruned_input_model,
pruned_output_model,
transformed_input_model
)
100 changes: 95 additions & 5 deletions tests/contract/test_resource_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@
"handlers": {"create": {}, "delete": {}, "read": {}},
}

SCHEMA_WITH_PROPERTY_TRANSFORM = {
"properties": {
"a": {"type": "string"},
"b": {"$ref": "#/definitions/c"},
},
"definitions": {
"c": {
"type": "object",
"properties": {"d": {"type": "String"}, "e": {"type": "integer"}},
}
},
"readOnlyProperties": ["/properties/a"],
"primaryIdentifier": ["/properties/a"],
"handlers": {"create": {}, "delete": {}, "read": {}},
"propertyTransform": {"/properties/b/c/d": '.b.c.d + "Test"'},
}

EMPTY_SCHEMA = {"handlers": {"create": [], "delete": [], "read": []}}


Expand Down Expand Up @@ -334,6 +351,45 @@ def resource_client_inputs_composite_key():
return client


@pytest.fixture
def resource_client_inputs_property_transform():
endpoint = "https://"
patch_sesh = patch(
"rpdk.core.contract.resource_client.create_sdk_session", autospec=True
)
patch_creds = patch(
"rpdk.core.contract.resource_client.get_temporary_credentials",
autospec=True,
return_value={},
)
patch_account = patch(
"rpdk.core.contract.resource_client.get_account",
autospec=True,
return_value=ACCOUNT,
)
with patch_sesh as mock_create_sesh, patch_creds as mock_creds:
with patch_account as mock_account:
mock_sesh = mock_create_sesh.return_value
mock_sesh.region_name = DEFAULT_REGION
client = ResourceClient(
DEFAULT_FUNCTION,
endpoint,
DEFAULT_REGION,
SCHEMA_WITH_PROPERTY_TRANSFORM,
EMPTY_OVERRIDE,
)

mock_sesh.client.assert_called_once_with("lambda", endpoint_url=endpoint)
mock_creds.assert_called_once_with(mock_sesh, LOWER_CAMEL_CRED_KEYS, None)
mock_account.assert_called_once_with(mock_sesh, {})
assert client._function_name == DEFAULT_FUNCTION
assert client._schema == SCHEMA_WITH_PROPERTY_TRANSFORM
assert client._overrides == EMPTY_OVERRIDE
assert client.account == ACCOUNT

return client


def test_prune_properties():
document = {
"foo": "bar",
Expand Down Expand Up @@ -692,6 +748,40 @@ def test_update_schema(resource_client):
assert resource_client.create_only_paths == {("properties", "d")}


def test_transform_model(resource_client_inputs_property_transform):
inputs = {"a": "ValueA", "b": {"c": {"d": "ValueD", "e": 1}}}
transformed_inputs = {"a": "ValueA", "b": {"c": {"d": "ValueDTest", "e": 1}}}

resource_client_inputs_property_transform.transform_model(inputs)

assert inputs == transformed_inputs


def test_compare_with_transform_should_pass(resource_client_inputs_property_transform):
inputs = {"a": "ValueA", "b": {"c": {"d": "ValueD", "e": 1}}}
transformed_inputs = {"a": "ValueA", "b": {"c": {"d": "ValueDTest", "e": 1}}}
outputs = {"a": "ValueA", "b": {"c": {"d": "ValueDTest", "e": 1}}}

resource_client_inputs_property_transform.compare(
inputs, outputs, transformed_inputs
)


def test_compare_with_transform_should_throw_exception(
resource_client_inputs_property_transform,
):
inputs = {"a": "ValueA", "b": {"c": {"d": "ValueD", "e": 1}}}
transformed_inputs = {"a": "ValueA", "b": {"c": {"d": "ValueDTest", "e": 1}}}
outputs = {"a": "ValueA", "b": {"c": {"d": "D", "e": 1}}}

try:
resource_client_inputs_property_transform.compare(
inputs, outputs, transformed_inputs
)
except AssertionError:
logging.debug("This test expects Assertion Exception to be thrown")


def test_strategy(resource_client):
schema = {
"properties": {
Expand Down Expand Up @@ -1566,7 +1656,7 @@ def test_compare_should_pass(resource_client):
"h": [{"d": 1, "e": 3}, {"d": 2}],
"i": ["abc", "ghi"],
}
resource_client.compare(inputs, outputs)
resource_client.compare(inputs, outputs, None)


def test_compare_should_throw_exception(resource_client):
Expand All @@ -1579,7 +1669,7 @@ def test_compare_should_throw_exception(resource_client):
"h": [{"d": 1}],
}
try:
resource_client.compare(inputs, outputs)
resource_client.compare(inputs, outputs, None)
except AssertionError:
logging.debug("This test expects Assertion Exception to be thrown")

Expand Down Expand Up @@ -1715,7 +1805,7 @@ def test_compare_should_throw_exception(resource_client):
def test_compare_collection(resource_client, inputs, outputs, schema_fragment):
resource_client._update_schema(schema_fragment)

resource_client.compare(inputs, outputs)
resource_client.compare(inputs, outputs, None)


def test_compare_should_throw_key_error(resource_client):
Expand All @@ -1724,7 +1814,7 @@ def test_compare_should_throw_key_error(resource_client):

outputs = {"b": {"d": 1, "e": 2}, "f": [{"d": 1, "e": 2}, {"d": 2, "e": 3}]}
try:
resource_client.compare(inputs, outputs)
resource_client.compare(inputs, outputs, None)
except AssertionError:
logging.debug("This test expects Assertion Exception to be thrown")

Expand All @@ -1739,6 +1829,6 @@ def test_compare_ordered_list_throws_assertion_exception(resource_client):
"i": ["abc", "ghi", "tt"],
}
try:
resource_client.compare(inputs, outputs)
resource_client.compare(inputs, outputs, None)
except AssertionError:
logging.debug("This test expects Assertion Exception to be thrown")

0 comments on commit e6cadc4

Please sign in to comment.