From 9990b8b829be52a67b71701005d8388fd4abe32c Mon Sep 17 00:00:00 2001 From: Olivier Desenfans Date: Fri, 7 Oct 2022 00:08:59 +0200 Subject: [PATCH] Internal: add unit tests for the aggregates endpoint Problem: the /api/v0/aggregates/{address}.json endpoint was not tested enough. Solution: add tests. --- tests/api/conftest.py | 22 +++- tests/api/fixtures/fixture_aggregates.json | 123 ++++++++++++++++++ tests/api/test_aggregates.py | 141 +++++++++++++++++++++ 3 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 tests/api/fixtures/fixture_aggregates.json create mode 100644 tests/api/test_aggregates.py diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 7dd3c87cf..20e6ab15b 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -1,16 +1,32 @@ import json from pathlib import Path +from typing import Dict, List + import pytest_asyncio from aleph.model.messages import Message -@pytest_asyncio.fixture -async def fixture_messages(test_db): +async def _load_fixtures(filename: str): fixtures_dir = Path(__file__).parent / "fixtures" - fixtures_file = fixtures_dir / "fixture_messages.json" + fixtures_file = fixtures_dir / filename with fixtures_file.open() as f: messages = json.load(f) await Message.collection.insert_many(messages) return messages + + +@pytest_asyncio.fixture +async def fixture_messages(test_db) -> List[Dict]: + return await _load_fixtures("fixture_messages.json") + + +@pytest_asyncio.fixture +async def fixture_aggregate_messages(test_db) -> List[Dict]: + return await _load_fixtures("fixture_aggregates.json") + + +@pytest_asyncio.fixture +async def fixture_post_messages(test_db) -> List[Dict]: + return await _load_fixtures("fixture_posts.json") diff --git a/tests/api/fixtures/fixture_aggregates.json b/tests/api/fixtures/fixture_aggregates.json new file mode 100644 index 000000000..c488e78e8 --- /dev/null +++ b/tests/api/fixtures/fixture_aggregates.json @@ -0,0 +1,123 @@ +[ + { + "chain": "ETH", + "item_hash": "53c2b16aa84b10878982a2920844625546f5db32337ecd9dd15928095a30381c", + "sender": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "type": "AGGREGATE", + "channel": "INTEGRATION_TESTS", + "content": { + "address": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "time": 1644857371.391834, + "key": "test_reference", + "content": { + "a": 1, + "b": 2 + } + }, + "item_content": "{\"address\":\"0x720F319A9c3226dCDd7D8C49163D79EDa1084E98\",\"time\":1644857371.391834,\"key\":\"test_reference\",\"content\":{\"a\":1,\"b\":2}}", + "item_type": "inline", + "signature": "0x7eee4cfc03b963ec51f04f60f6f7d58b0f24e0309d209feecb55af9e411ed1c01cfb547bb13539e91308b044c3661d93ddf272426542bc1a47722614cb0cd3621c", + "size": 128, + "time": 1644859283.101 + }, + { + "chain": "ETH", + "item_hash": "0022ed09d16a1c3d6cbb3c7e2645657ebaa0382eba65be06264b106f528b85bf", + "sender": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "type": "AGGREGATE", + "channel": "INTEGRATION_TESTS", + "content": { + "address": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "time": 1644857704.6253593, + "key": "test_reference", + "content": { + "c": 3, + "d": 4 + } + }, + "item_content": "{\"address\":\"0x720F319A9c3226dCDd7D8C49163D79EDa1084E98\",\"time\":1644857704.6253593,\"key\":\"test_reference\",\"content\":{\"c\":3,\"d\":4}}", + "item_type": "inline", + "signature": "0xe6129196c36b066302692b53bcb78a9d8c996854b170238ebfe56924f0b6be604883c30a66d75250de489e1edb683c7da8ddb1ccb50a39d1bbbdad617e5c958f1b", + "size": 129, + "time": 1644859283.12 + }, + { + "chain": "ETH", + "item_hash": "a87004aa03f8ae63d2c4bbe84b93b9ce70ca6482ce36c82ab0b0f689fc273f34", + "sender": "0xaC033C1cA5C49Eff98A1D9a56BeDBC4840010BA4", + "type": "AGGREGATE", + "channel": "INTEGRATION_TESTS", + "content": { + "address": "0xaC033C1cA5C49Eff98A1D9a56BeDBC4840010BA4", + "time": 1648215802.3821976, + "key": "test_reference", + "content": { + "c": 3, + "d": 4 + } + }, + "item_content": "{\"address\":\"0xaC033C1cA5C49Eff98A1D9a56BeDBC4840010BA4\",\"time\":1648215802.3821976,\"key\":\"test_reference\",\"content\":{\"c\":3,\"d\":4}}", + "item_type": "inline", + "signature": "0xc0f6ce2e4e9561b3949d51a97b8746125e1f031bbc13813cc74f1f61eea654fe300ad5e9ec098d41374bc0e43f83f2d66b834672abb811ae6a2dcdbd09f2565f1c", + "size": 129, + "time": 1648467547.771 + }, + { + "chain": "ETH", + "item_hash": "f875631a6c4a70ce44143bdd9a64861a5ce6f68e2267a00979ff0ad399a6c780", + "sender": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "type": "AGGREGATE", + "channel": "INTEGRATION_TESTS", + "confirmations": [ + { + "chain": "ETH", + "height": 14205580, + "hash": "0x234b3cb25e893780c4cf50ec82c4ae9a61d61b766a367b559ae8192463e84a1b" + } + ], + "confirmed": true, + "content": { + "address": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "time": 1644857371.1748412, + "key": "test_target", + "content": { + "a": 1, + "b": 2 + } + }, + "item_content": "{\"address\":\"0x720F319A9c3226dCDd7D8C49163D79EDa1084E98\",\"time\":1644857371.1748412,\"key\":\"test_target\",\"content\":{\"a\":1,\"b\":2}}", + "item_type": "inline", + "signature": "0xaa28dafaecfd063bd30f65c877260bcdab37931fe7d8ef13173a952ae57a79e544d9fc9ae9131ba6ce6638bdbd62996467eb4a999416603ff2d1eaff372427bd1b", + "size": 126, + "time": 1644859283.1 + }, + { + "chain": "ETH", + "item_hash": "8c83e020b1f0661de3238ecaf2a41fd2f9dfe4a6c56453ccdf3ddd3fa4fae147", + "sender": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "type": "AGGREGATE", + "channel": "INTEGRATION_TESTS", + "confirmations": [ + { + "chain": "ETH", + "height": 14205311, + "hash": "0x805dcf51856c813d5524f5b64555145fce6487b81dc605e6657fd208bebb2e05" + } + ], + "confirmed": true, + "content": { + "address": "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98", + "time": 1644853185.0710306, + "key": "test_key", + "content": { + "a": 1, + "b": 2 + } + }, + "item_content": "{\"address\":\"0x720F319A9c3226dCDd7D8C49163D79EDa1084E98\",\"time\":1644853185.0710306,\"key\":\"test_key\",\"content\":{\"a\":1,\"b\":2}}", + "item_type": "inline", + "signature": "0x4e3060c596de77b19f2791fbc34eff3d6c89c63d29250c960e9c1b752898d22d0f7d7759dc0b0d935b93e29534e6861d7a8deeb75cd69836acf0ad0e6e8626601b", + "size": 123, + "time": 1644855661.089 + } +] diff --git a/tests/api/test_aggregates.py b/tests/api/test_aggregates.py new file mode 100644 index 000000000..67cfd7966 --- /dev/null +++ b/tests/api/test_aggregates.py @@ -0,0 +1,141 @@ +import itertools +from typing import Dict, Iterable, List + +import aiohttp +import pytest + +AGGREGATES_URI = "/api/v0/aggregates/{address}.json" + +# Another address with three aggregates +ADDRESS_1 = "0x720F319A9c3226dCDd7D8C49163D79EDa1084E98" +# Another address with one aggregate +ADDRESS_2 = "0xaC033C1cA5C49Eff98A1D9a56BeDBC4840010BA4" + +EXPECTED_AGGREGATES = { + ADDRESS_1: { + "test_key": {"a": 1, "b": 2}, + "test_target": {"a": 1, "b": 2}, + "test_reference": {"a": 1, "b": 2, "c": 3, "d": 4}, + }, + ADDRESS_2: {"test_reference": {"c": 3, "d": 4}}, +} + + +def make_uri(address: str) -> str: + return AGGREGATES_URI.format(address=address) + + +def assert_aggregates_equal(expected: List[Dict], actual: Dict[str, Dict]): + for expected_aggregate in expected: + aggregate = actual[expected_aggregate["content"]["key"]] + assert "_id" not in aggregate + + assert aggregate == expected_aggregate["content"]["content"] + + +def merge_aggregates(messages: Iterable[Dict]) -> List[Dict]: + def merge_content(_messages: List[Dict]) -> Dict: + original = _messages[0] + for update in _messages[1:]: + original["content"]["content"].update(update["content"]["content"]) + return original + + aggregates = [] + + for key, group in itertools.groupby( + sorted(messages, key=lambda msg: msg["content"]["key"]), + lambda msg: msg["content"]["key"], + ): + sorted_messages = sorted(group, key=lambda msg: msg["time"]) + aggregates.append(merge_content(sorted_messages)) + + return aggregates + + +async def get_aggregates(api_client, address: str, **params) -> aiohttp.ClientResponse: + return await api_client.get(make_uri(address), params=params) + + +async def get_aggregates_expect_success(api_client, address: str, **params): + response = await get_aggregates(api_client, address, **params) + assert response.status == 200, await response.text() + return await response.json() + + +@pytest.fixture() +def fixture_aggregates(fixture_aggregate_messages): + return merge_aggregates(fixture_aggregate_messages) + + +@pytest.mark.asyncio +async def test_get_aggregates_no_update(ccn_api_client, fixture_aggregates): + """ + Tests receiving an aggregate from an address which posted one aggregate and never + updated it. + """ + + address = ADDRESS_2 + aggregates = await get_aggregates_expect_success(ccn_api_client, address) + + assert aggregates["address"] == address + assert aggregates["data"] == EXPECTED_AGGREGATES[address] + + +@pytest.mark.asyncio +async def test_get_aggregates(ccn_api_client, fixture_aggregates: List[Dict]): + """ + A more complex case with 3 aggregates, one of which was updated. + """ + + address = ADDRESS_1 + aggregates = await get_aggregates_expect_success(ccn_api_client, address) + + assert address == aggregates["address"] + assert aggregates["data"]["test_key"] == {"a": 1, "b": 2} + assert aggregates["data"]["test_target"] == {"a": 1, "b": 2} + assert aggregates["data"]["test_reference"] == {"a": 1, "b": 2, "c": 3, "d": 4} + + assert_aggregates_equal(fixture_aggregates, aggregates["data"]) + + +@pytest.mark.asyncio +async def test_get_aggregates_filter_by_key(ccn_api_client, fixture_aggregates: List[Dict]): + """ + Tests the 'keys' query parameter. + """ + + address, key = ADDRESS_1, "test_target" + aggregates = await get_aggregates_expect_success(ccn_api_client, address=address, keys=key) + assert aggregates["address"] == address + assert aggregates["data"][key] == EXPECTED_AGGREGATES[address][key] + + # Multiple keys + address, keys = ADDRESS_1, ["test_target", "test_reference"] + aggregates = await get_aggregates_expect_success(ccn_api_client, address=address, keys=",".join(keys)) + assert aggregates["address"] == address + for key in keys: + assert aggregates["data"][key] == EXPECTED_AGGREGATES[address][key], f"Key {key} does not match" + + +@pytest.mark.asyncio +async def test_get_aggregates_limit(ccn_api_client, fixture_aggregates: List[Dict]): + """ + Tests the 'limit' query parameter. + """ + + address, key = ADDRESS_1, "test_reference" + aggregates = await get_aggregates_expect_success(ccn_api_client, address=address, keys=key, limit=1) + assert aggregates["address"] == address + assert aggregates["data"][key] == {"c": 3, "d": 4} + + +@pytest.mark.asyncio +async def test_get_aggregates_invalid_address(ccn_api_client, fixture_aggregates: List[Dict]): + """ + Pass an unknown address. + """ + + invalid_address = "unknown" + + response = await get_aggregates(ccn_api_client, invalid_address) + assert response.status == 404