diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index bff6a227..e27f7d77 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -195,14 +195,12 @@ async def get_message( self, item_hash: str, message_type: Optional[Type[GenericMessage]] = None, - channel: Optional[str] = None, ) -> GenericMessage: """ Get a single message from its `item_hash` and perform some basic validation. :param item_hash: Hash of the message to fetch :param message_type: Type of message to fetch - :param channel: Channel of the message to fetch """ pass diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 93cbe837..97ad0c08 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -9,7 +9,7 @@ from pydantic import ValidationError from ..conf import settings -from ..exceptions import FileTooLarge, MessageNotFoundError, MultipleMessagesError +from ..exceptions import FileTooLarge, ForgottenMessageError, MessageNotFoundError from ..query.filters import MessageFilter, PostFilter from ..query.responses import MessagesResponse, Post, PostsResponse from ..types import GenericMessage @@ -290,21 +290,20 @@ async def get_message( self, item_hash: str, message_type: Optional[Type[GenericMessage]] = None, - channel: Optional[str] = None, ) -> GenericMessage: - messages_response = await self.get_messages( - message_filter=MessageFilter( - hashes=[item_hash], - channels=[channel] if channel else None, + async with self.http_session.get(f"/api/v0/messages/{item_hash}") as resp: + try: + resp.raise_for_status() + except aiohttp.ClientResponseError as e: + if e.status == 404: + raise MessageNotFoundError(f"No such hash {item_hash}") + raise e + message_raw = await resp.json() + if message_raw["status"] == "forgotten": + raise ForgottenMessageError( + f"The requested message {message_raw['item_hash']} has been forgotten by {', '.join(message_raw['forgotten_by'])}" ) - ) - if len(messages_response.messages) < 1: - raise MessageNotFoundError(f"No such hash {item_hash}") - if len(messages_response.messages) != 1: - raise MultipleMessagesError( - f"Multiple messages found for the same item_hash `{item_hash}`" - ) - message: GenericMessage = messages_response.messages[0] + message = parse_message(message_raw["message"]) if message_type: expected_type = get_message_type_value(message_type) if message.type != expected_type: diff --git a/src/aleph/sdk/exceptions.py b/src/aleph/sdk/exceptions.py index 7ac7ae89..f2cd96d6 100644 --- a/src/aleph/sdk/exceptions.py +++ b/src/aleph/sdk/exceptions.py @@ -56,3 +56,9 @@ class DomainConfigurationError(Exception): """Raised when the domain checks are not satisfied""" pass + + +class ForgottenMessageError(QueryError): + """The requested message was forgotten""" + + pass diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a51b1483..552cb548 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,6 +2,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Any, Callable, Dict, List +from unittest.mock import AsyncMock, MagicMock import pytest as pytest from aleph_message.models import AggregateMessage, AlephMessage, PostMessage @@ -10,7 +11,9 @@ import aleph.sdk.chains.sol as solana import aleph.sdk.chains.substrate as substrate import aleph.sdk.chains.tezos as tezos +from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.chains.common import get_fallback_private_key +from aleph.sdk.types import Account @pytest.fixture @@ -111,6 +114,12 @@ def aleph_messages() -> List[AlephMessage]: ] +@pytest.fixture +def json_post() -> dict: + with open(Path(__file__).parent / "post.json", "r") as f: + return json.load(f) + + @pytest.fixture def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]: return lambda page: { @@ -122,3 +131,85 @@ def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]: "pagination_per_page": max(len(aleph_messages), 20), "pagination_total": len(aleph_messages) if page == 1 else 0, } + + +@pytest.fixture +def raw_posts_response(json_post) -> Callable[[int], Dict[str, Any]]: + return lambda page: { + "posts": [json_post] if int(page) == 1 else [], + "pagination_item": "posts", + "pagination_page": int(page), + "pagination_per_page": 1, + "pagination_total": 1 if page == 1 else 0, + } + + +class MockResponse: + def __init__(self, sync: bool): + self.sync = sync + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + ... + + async def raise_for_status(self): + ... + + @property + def status(self): + return 200 if self.sync else 202 + + async def json(self): + message_status = "processed" if self.sync else "pending" + return { + "message_status": message_status, + "publication_status": {"status": "success", "failed": []}, + } + + async def text(self): + return json.dumps(await self.json()) + + +@pytest.fixture +def mock_session_with_post_success( + ethereum_account: Account, +) -> AuthenticatedAlephHttpClient: + http_session = AsyncMock() + http_session.post = MagicMock() + http_session.post.side_effect = lambda *args, **kwargs: MockResponse( + sync=kwargs.get("sync", False) + ) + + client = AuthenticatedAlephHttpClient( + account=ethereum_account, api_server="http://localhost" + ) + client.http_session = http_session + + return client + + +def make_custom_mock_response(resp_json, status=200) -> MockResponse: + class CustomMockResponse(MockResponse): + async def json(self): + return resp_json + + @property + def status(self): + return status + + return CustomMockResponse(sync=True) + + +def make_mock_get_session(get_return_value: Dict[str, Any]) -> AlephHttpClient: + class MockHttpSession(AsyncMock): + def get(self, *_args, **_kwargs): + return make_custom_mock_response(get_return_value) + + http_session = MockHttpSession() + + client = AlephHttpClient(api_server="http://localhost") + client.http_session = http_session + + return client diff --git a/tests/unit/post.json b/tests/unit/post.json new file mode 100644 index 00000000..533fe24e --- /dev/null +++ b/tests/unit/post.json @@ -0,0 +1,49 @@ +{ + "chain": "ETH", + "item_hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe", + "sender": "0x4D52380D3191274a04846c89c069E6C3F2Ed94e4", + "type": "aleph-network-metrics", + "channel": "aleph-scoring", + "confirmed": false, + "content": { + "tags": [ + "mainnet" + ], + "metrics": { + "ccn": [ + { + "asn": 24940, + "url": "http://135.181.165.203:4024/", + "as_name": "HETZNER-AS, DE", + "node_id": "599de3dc1857b73d33bf616ab2f449df579e2f1270c9b04dc7bdc630524e1e6c", + "version": "v0.5.1", + "txs_total": 0, + "measured_at": 1700562026.269039, + "base_latency": 0.09740376472473145, + "metrics_latency": 0.3925642967224121, + "pending_messages": 0, + "aggregate_latency": 0.06854844093322754, + "base_latency_ipv4": 0.09740376472473145, + "eth_height_remaining": 0, + "file_download_latency": 0.10360932350158691 + } + ], + "server": "151.115.63.76", + "server_asn": 12876, + "server_as_name": "Online SAS, FR" + }, + "version": "1.0" + }, + "item_content": null, + "item_type": "storage", + "signature": "0xc38c0ca2d683b2d0c629a640c156fbbce771c1d58d4c6f266bfa234f68b93302021981a9905d768510fb7fee050b6d5e48096258a2fec2aa531cc7594a4ede3e1b", + "size": 125810, + "time": 1700562222.942672, + "confirmations": [], + "original_item_hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe", + "original_signature": "0xc38c0ca2d683b2d0c629a640c156fbbce771c1d58d4c6f266bfa234f68b93302021981a9905d768510fb7fee050b6d5e48096258a2fec2aa531cc7594a4ede3e1b", + "original_type": "aleph-network-metrics", + "hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe", + "address": "0x4D52380D3191274a04846c89c069E6C3F2Ed94e4", + "ref": null +} \ No newline at end of file diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index 104482da..1d3f4339 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -1,5 +1,4 @@ -import json -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock import pytest as pytest from aleph_message.models import ( @@ -12,53 +11,7 @@ ) from aleph_message.status import MessageStatus -from aleph.sdk.client import AuthenticatedAlephHttpClient -from aleph.sdk.types import Account, StorageEnum - - -@pytest.fixture -def mock_session_with_post_success( - ethereum_account: Account, -) -> AuthenticatedAlephHttpClient: - class MockResponse: - def __init__(self, sync: bool): - self.sync = sync - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - ... - - @property - def status(self): - return 200 if self.sync else 202 - - async def raise_for_status(self): - ... - - async def json(self): - message_status = "processed" if self.sync else "pending" - return { - "message_status": message_status, - "publication_status": {"status": "success", "failed": []}, - } - - async def text(self): - return json.dumps(await self.json()) - - http_session = AsyncMock() - http_session.post = MagicMock() - http_session.post.side_effect = lambda *args, **kwargs: MockResponse( - sync=kwargs.get("sync", False) - ) - - client = AuthenticatedAlephHttpClient( - account=ethereum_account, api_server="http://localhost" - ) - client.http_session = http_session - - return client +from aleph.sdk.types import StorageEnum @pytest.mark.asyncio diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index 08486784..cef01a5c 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -1,50 +1,18 @@ import unittest from datetime import datetime -from typing import Any, Dict -from unittest.mock import AsyncMock import pytest from aleph_message.models import MessagesResponse, MessageType -from aleph.sdk import AlephHttpClient -from aleph.sdk.conf import settings +from aleph.sdk.exceptions import ForgottenMessageError from aleph.sdk.query.filters import MessageFilter, PostFilter from aleph.sdk.query.responses import PostsResponse - - -def make_mock_session(get_return_value: Dict[str, Any]) -> AlephHttpClient: - class MockResponse: - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - ... - - @property - def status(self): - return 200 - - def raise_for_status(self): - ... - - async def json(self): - return get_return_value - - class MockHttpSession(AsyncMock): - def get(self, *_args, **_kwargs): - return MockResponse() - - http_session = MockHttpSession() - - client = AlephHttpClient(api_server="http://localhost") - client.http_session = http_session - - return client +from tests.unit.conftest import make_mock_get_session @pytest.mark.asyncio async def test_fetch_aggregate(): - mock_session = make_mock_session( + mock_session = make_mock_get_session( {"data": {"corechannel": {"nodes": [], "resource_nodes": []}}} ) async with mock_session: @@ -57,7 +25,7 @@ async def test_fetch_aggregate(): @pytest.mark.asyncio async def test_fetch_aggregates(): - mock_session = make_mock_session( + mock_session = make_mock_get_session( {"data": {"corechannel": {"nodes": [], "resource_nodes": []}}} ) @@ -70,36 +38,52 @@ async def test_fetch_aggregates(): @pytest.mark.asyncio -async def test_get_posts(): - async with AlephHttpClient(api_server=settings.API_HOST) as session: +async def test_get_posts(raw_posts_response): + mock_session = make_mock_get_session(raw_posts_response(1)) + post = raw_posts_response(1)["posts"][0] + async with mock_session as session: response: PostsResponse = await session.get_posts( - page_size=2, + page=1, + page_size=1, post_filter=PostFilter( - channels=["TEST"], - start_date=datetime(2021, 1, 1), + channels=post["channel"], + start_date=datetime.fromtimestamp(post["time"]), ), + ignore_invalid_messages=False, ) posts = response.posts - assert len(posts) > 1 + assert len(posts) == 1 @pytest.mark.asyncio -async def test_get_messages(): - async with AlephHttpClient(api_server=settings.API_HOST) as session: +async def test_get_messages(raw_messages_response): + mock_session = make_mock_get_session(raw_messages_response(1)) + async with mock_session as session: response: MessagesResponse = await session.get_messages( page_size=2, message_filter=MessageFilter( message_types=[MessageType.post], start_date=datetime(2021, 1, 1), ), + ignore_invalid_messages=False, ) messages = response.messages - assert len(messages) > 1 + assert len(messages) >= 1 assert messages[0].type assert messages[0].sender +@pytest.mark.asyncio +async def test_get_forgotten_message(): + mock_session = make_mock_get_session( + {"status": "forgotten", "item_hash": "cafebabe", "forgotten_by": "OxBEEFDAD"} + ) + async with mock_session as session: + with pytest.raises(ForgottenMessageError): + await session.get_message("cafebabe") + + if __name__ == "__main __": unittest.main()