From b47709bdc1bc570d058ca6ca7dc36c8a618bbf2b Mon Sep 17 00:00:00 2001 From: Frederic Samier Date: Tue, 8 Oct 2024 14:10:15 +0200 Subject: [PATCH] test: adapt dictionaries before comparing in roundtrip unit-tests --- .../convert/test_convert_eip712_round_trip.py | 68 ++++++++++++++++++- tests/dict_utils.py | 33 +++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/dict_utils.py diff --git a/tests/convert/test_convert_eip712_round_trip.py b/tests/convert/test_convert_eip712_round_trip.py index 0d8f260..bd1b1d2 100644 --- a/tests/convert/test_convert_eip712_round_trip.py +++ b/tests/convert/test_convert_eip712_round_trip.py @@ -1,19 +1,81 @@ +import json from pathlib import Path +from typing import Any import pytest from eip712 import EIP712DAppDescriptor -from erc7730.common.pydantic import model_from_json_file_with_includes +from erc7730.common.pydantic import model_from_json_file_with_includes, model_to_json_str from erc7730.convert.convert import convert_and_print_errors from erc7730.convert.convert_eip712_to_erc7730 import EIP712toERC7730Converter from erc7730.convert.convert_erc7730_input_to_resolved import ERC7730InputToResolved from erc7730.convert.convert_erc7730_to_eip712 import ERC7730toEIP712Converter +from erc7730.model.display import FieldFormat from erc7730.model.input.descriptor import InputERC7730Descriptor -from tests.assertions import assert_model_json_equals +from tests.assertions import assert_dict_equals, assert_model_json_equals from tests.cases import path_id +from tests.dict_utils import del_by_path, is_in_path, map_by_path from tests.files import ERC7730_EIP712_DESCRIPTORS, LEGACY_EIP712_DESCRIPTORS +def _adapt_and_comapre(input: InputERC7730Descriptor, output: InputERC7730Descriptor) -> None: + input_dict, output_dict = json.loads(model_to_json_str(input)), json.loads(model_to_json_str(output)) + + # $schema is not present in EIP-712 + del_by_path(input_dict, "$schema") + + # Addresses may be in EIP-55 format, convert to lower case in input + def _lowercase_addresses(deployments: list[dict[str, Any]]) -> Any: + for deployment in deployments: + if "address" in deployment: + deployment["address"] = deployment["address"].lower() + return deployments + + map_by_path( + input_dict, + _lowercase_addresses, + "context", + "eip712", + "deployments", + ) + + # chainId and verifyingContract are always generated by the converter + # even when not explicitly present in the input + for key in "chainId", "verifyingContract": + path = "context", "eip712", "domain", key + if not is_in_path(input_dict, *path): + del_by_path(output_dict, *path) + + # Some metadata cannot be stored in EIP712, and thus are lost with the roundtrip + del_by_path(input_dict, "metadata", "info") + del_by_path(input_dict, "metadata", "token") + del_by_path(input_dict, "metadata", "token") + del_by_path(input_dict, "context", "eip712", "domain", "version") + + # Adapt formats objects to match EIP-712 conversion + def _cleanup_formats(formats: dict[str, Any]) -> Any: + for _, message in formats.items(): + # Remove ERC-7730 specific fields + del_by_path(message, "$id") + del_by_path(message, "required") + del_by_path(message, "screens") + if "fields" in message: + for field in message["fields"]: + # Other formats are always converted to RAW + if "format" in field and field["format"] not in ( + FieldFormat.AMOUNT, + FieldFormat.TOKEN_AMOUNT, + FieldFormat.DATE, + ): + field["format"] = "raw" + + return formats + + map_by_path(input_dict, _cleanup_formats, "display", "formats") + + assert_dict_equals(input_dict, output_dict) + + @pytest.mark.parametrize("input_file", ERC7730_EIP712_DESCRIPTORS, ids=path_id) def test_roundtrip_from_erc7730(input_file: Path) -> None: input_erc7730_descriptor = InputERC7730Descriptor.load(input_file) @@ -29,7 +91,7 @@ def test_roundtrip_from_erc7730(input_file: Path) -> None: assert output_erc7730_descriptor is not None if isinstance(output_erc7730_descriptor, dict): pytest.skip("Multiple descriptors tests not supported") - assert_model_json_equals(input_erc7730_descriptor, output_erc7730_descriptor) + _adapt_and_comapre(input_erc7730_descriptor, output_erc7730_descriptor) @pytest.mark.parametrize("input_file", LEGACY_EIP712_DESCRIPTORS, ids=path_id) diff --git a/tests/dict_utils.py b/tests/dict_utils.py new file mode 100644 index 0000000..2f03ab4 --- /dev/null +++ b/tests/dict_utils.py @@ -0,0 +1,33 @@ +import contextlib +import operator +from collections.abc import Callable +from functools import reduce +from typing import Any + + +def get_by_path(root: dict[str, Any], *paths: str) -> Any: + """Access a nested object in root by item sequence.""" + return reduce(operator.getitem, list(paths), root) + + +def is_in_path(root: dict[str, Any], *paths: str) -> bool: + """Check if a key-value in a nested object in root by item sequence exists.""" + try: + get_by_path(root, *paths) + return True + except KeyError: + return False + + +def del_by_path(root: dict[str, Any], *paths: str) -> None: + """Delete a key-value in a nested object in root by item sequence.""" + path_list = list(paths) + with contextlib.suppress(KeyError): + del get_by_path(root, *path_list[:-1])[path_list[-1]] + + +def map_by_path(root: dict[str, Any], func: Callable[[Any], Any], *paths: str) -> None: + """Map a function to a key-value in a nested object in root by item sequence.""" + path_list = list(paths) + with contextlib.suppress(KeyError): + get_by_path(root, *path_list[:-1])[path_list[-1]] = func(get_by_path(root, *paths))