From 6379b3effc404e8a4fe6d6ce7ec3cb653546b8cf Mon Sep 17 00:00:00 2001 From: Bryan White Date: Wed, 23 Nov 2022 18:54:05 +0100 Subject: [PATCH] tests: almanac registrations --- src/genesis/helpers/field_enums.py | 30 +++++ tests/e2e/entities/test_almanac.py | 189 +++++++++++++++++++++++++++++ tests/helpers/contracts.py | 37 ++++++ tests/helpers/graphql.py | 3 +- 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/entities/test_almanac.py diff --git a/src/genesis/helpers/field_enums.py b/src/genesis/helpers/field_enums.py index a09cc9011..f03d43143 100644 --- a/src/genesis/helpers/field_enums.py +++ b/src/genesis/helpers/field_enums.py @@ -325,3 +325,33 @@ class AuthzExecMessageFields(NamedFields): @property def table(self): return "authz_exec_messages" + + +class AlmanacRegistrations(NamedFields): + id = 0 + expiry_height = 1 + account_id = 2 + record_id = 3 + transaction_id = 4 + block_id = 5 +# event_id = 6 +# record_id = 7 + + @classmethod + @property + def table(self): + return "almanac_registrations" + + + +class AlmanacRecords(NamedFields): + id = 0 + service = 1 + transaction_id = 2 + block_id = 3 +# event_id = 4 + + @classmethod + @property + def table(self): + return "almanac_records" diff --git a/tests/e2e/entities/test_almanac.py b/tests/e2e/entities/test_almanac.py new file mode 100644 index 000000000..4bedd6008 --- /dev/null +++ b/tests/e2e/entities/test_almanac.py @@ -0,0 +1,189 @@ +import json +import sys +import time +import unittest +from dataclasses import dataclass +from pathlib import Path +from typing import List, Dict + +import graphql +from cosmpy.aerial.tx_helpers import SubmittedTx +from gql import gql + +repo_root_path = Path(__file__).parent.parent.parent.parent.absolute() +sys.path.insert(0, str(repo_root_path)) + +from src.genesis.helpers.field_enums import AlmanacRegistrations, AlmanacRecords +from tests.helpers.contracts import AlmanacContract, DefaultAlmanacContractConfig +from tests.helpers.entity_test import EntityTest +from tests.helpers.graphql import test_filtered_query +from tests.helpers.regexes import msg_id_regex, tx_id_regex, block_id_regex + + +def by_expiry_height(registration_node: Dict): + return int(registration_node["expiryHeight"]) + + +@dataclass +class Scenario: + name: str + query: graphql.DocumentNode + expected: any + + +class TestAlmanac(EntityTest): + test_registrations_endpoints = [ + "127.0.0.1:9999", + "127.0.0.1:8888", + "127.0.0.1:7777", + "127.0.0.1:6666" + ] + submitted_txs: List[SubmittedTx] = [] + expected_registrations: List[Dict] = [] + expected_records: List[Dict] = [ + { + "service": { + "protocols": ["grpc"], + "endpoints": [{ + "url": endpoint, + # NB: not "proper" usage of weight; for testing only + "weight": i + }] + } + } for (i, endpoint) in enumerate(test_registrations_endpoints) + ] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.clean_db({"almanac_registrations", "almanac_resolutions"}) + cls._contract = AlmanacContract(cls.ledger_client, cls.validator_wallet) + + # NB: broadcast multiple registrations + for (i, expected_record) in enumerate(cls.expected_records): + tx = cls._contract.execute({ + "register": { + "record": expected_record, + # "sequence": , + # "signature": , + } + }, cls.validator_wallet, funds=DefaultAlmanacContractConfig.register_stake_funds) + tx.wait_to_complete() + cls.submitted_txs.append(tx) + cls.expected_registrations.append({ + "accountId": cls.validator_address, + "expiryHeight": tx.response.height + DefaultAlmanacContractConfig.expiry_height, + "record": expected_record + }) + # NB: wait for the indexer + time.sleep(7) + + def test_registrations_sql(self): + registrations = self.db_cursor.execute(AlmanacRegistrations.select_query()).fetchall() + actual_reg_length = len(registrations) + + expected_registrations_count = len(self.expected_registrations) + self.assertEqual(expected_registrations_count, + actual_reg_length, + f"expected {expected_registrations_count} registrations; got {actual_reg_length}") + for (i, registration) in enumerate(registrations): + self.assertEqual(self.validator_address, registration[AlmanacRegistrations.account_id.value]) + self.assertLess(self.submitted_txs[i].response.height, + registration[AlmanacRegistrations.expiry_height.value]) + self.assertRegex(registration[AlmanacRegistrations.id.value], msg_id_regex) + self.assertRegex(registration[AlmanacRegistrations.transaction_id.value], tx_id_regex) + self.assertRegex(registration[AlmanacRegistrations.block_id.value], block_id_regex) + + def matches_expected_record(_record: Dict) -> bool: + return _record["service"]["endpoints"][0]["weight"] == i + + # Lookup related record + record = self.db_cursor.execute( + AlmanacRecords.select_where( + f"almanac_records.id = '{registration[AlmanacRegistrations.record_id.value]}'", + [AlmanacRecords.table, AlmanacRegistrations.table])).fetchone() + expected_record = next(r for r in self.expected_records if matches_expected_record(r)) + self.assertIsNotNone(record) + self.assertIsNotNone(expected_record) + self.assertDictEqual(expected_record["service"], record[AlmanacRecords.service.value]) + self.assertRegex(record[AlmanacRecords.id.value], msg_id_regex) + self.assertRegex(record[AlmanacRecords.transaction_id.value], tx_id_regex) + self.assertRegex(record[AlmanacRecords.block_id.value], block_id_regex) + + def test_registrations_gql(self): + registrations_nodes = """ + { + id + expiryHeight + accountId + record { + id + service + # registrationId + # eventId + transactionId + blockId + } + transactionId + blockId + } + """ + + last_tx_height = self.submitted_txs[-1].response.height + expired_registrations_query = test_filtered_query("almanacRegistrations", { + "expiryHeight": { + "lessThanOrEqualTo": str(last_tx_height) + } + }, registrations_nodes) + + active_registrations_query = test_filtered_query("almanacRegistrations", { + "expiryHeight": { + "greaterThan": str(last_tx_height) + } + }, registrations_nodes) + + all_registrations_query = gql("query {almanacRegistrations {nodes " + registrations_nodes + "}}") + + last_expired_height = last_tx_height - DefaultAlmanacContractConfig.expiry_height + last_expired = next(r for r in self.submitted_txs if r.response.height == last_expired_height) + last_expired_index = self.submitted_txs.index(last_expired) + scenarios = [ + Scenario( + name="expired registrations", + query=expired_registrations_query, + expected=self.expected_registrations[0: last_expired_index + 1] + ), + Scenario( + name="active registrations", + query=active_registrations_query, + expected=self.expected_registrations[last_expired_index + 1:] + ), + Scenario( + name="all registrations", + query=all_registrations_query, + expected=self.expected_registrations + ), + ] + + for scenario in scenarios: + with self.subTest(scenario.name): + gql_result = self.gql_client.execute(scenario.query) + registrations = gql_result["almanacRegistrations"]["nodes"] + self.assertEqual(len(scenario.expected), len(registrations)) + + # TODO: use respective gql order by when available + # NB: sort by expiry height so that indexes match + # their respective scenario.expected index + list.sort(registrations, key=by_expiry_height) + self.assertEqual(len(scenario.expected), len(registrations)) + + for (i, registration) in enumerate(registrations): + self.assertRegex(registration["id"], msg_id_regex) + self.assertEqual(self.validator_address, registration["accountId"], ) + self.assertEqual(str(scenario.expected[i]["expiryHeight"]), registration["expiryHeight"]) + self.assertRegex(registration["transactionId"], tx_id_regex) + self.assertRegex(registration["blockId"], block_id_regex) + # TODO: assert record equality + +if __name__ == "__main__": + unittest.main() diff --git a/tests/helpers/contracts.py b/tests/helpers/contracts.py index b50b2c71c..c24ace81b 100644 --- a/tests/helpers/contracts.py +++ b/tests/helpers/contracts.py @@ -1,6 +1,8 @@ from dataclasses import dataclass import os +from typing import Optional, Union + import requests from cosmpy.aerial.client import LedgerClient from cosmpy.aerial.contract import LedgerContract @@ -22,6 +24,21 @@ class BridgeContractConfig: next_swap_id: int +@dataclass_json +@dataclass +class AlmanacContractConfig: + stake_denom: str + expiry_height: Optional[int] + register_stake_amount: Optional[str] + admin: Optional[str] + + @property + def register_stake_funds(self) -> Union[None, str]: + if self.register_stake_amount == "0": + return None + return self.register_stake_amount + self.stake_denom + + DefaultBridgeContractConfig = BridgeContractConfig( cap="250000000000000000000000000", reverse_aggregated_allowance="3000000000000000000000000", @@ -34,6 +51,13 @@ class BridgeContractConfig: next_swap_id=0 ) +DefaultAlmanacContractConfig = AlmanacContractConfig( + stake_denom="atestfet", + expiry_height=2, + register_stake_amount="0", + admin=None +) + def ensure_contract(name: str, url: str) -> str: contract_path = f".contract/{name}.wasm" @@ -112,3 +136,16 @@ def __init__(self, client: LedgerClient, admin: Wallet, cfg: BridgeContractConfi admin, store_gas_limit=3000000 ) + + +class AlmanacContract(LedgerContract): + def __init__(self, client: LedgerClient, admin: Wallet, cfg: AlmanacContractConfig = DefaultAlmanacContractConfig): + url = "https://github.com/fetchai/contract-agent-almanac/releases/download/v0.1.1/contract_agent_almanac.wasm" + contract_path = ensure_contract("almanac", url) + super().__init__(contract_path, client) + + self.deploy( + cfg.to_dict(), + admin, + store_gas_limit=3000000 + ) diff --git a/tests/helpers/graphql.py b/tests/helpers/graphql.py index 758d1de67..e70f13cec 100644 --- a/tests/helpers/graphql.py +++ b/tests/helpers/graphql.py @@ -2,6 +2,7 @@ import re from typing import Dict +import graphql from gql import gql json_keys_regex = re.compile('"(\w+)":') @@ -12,7 +13,7 @@ def to_gql(obj: Dict): return json_keys_regex.sub("\g<1>:", json.dumps(obj)) -def test_filtered_query(root_entity: str, _filter: Dict, nodes_string: str): +def test_filtered_query(root_entity: str, _filter: Dict, nodes_string: str) -> graphql.DocumentNode: filter_string = to_gql(_filter) return gql("""