Skip to content

Commit

Permalink
Refactor: Simplify and improve get_messages; use mocked responses (#81
Browse files Browse the repository at this point in the history
)

* Problem: Client tests were not fully offline

Solution: Mock responses; refactor to reduce duplication

* Problem: Client had no simple of finding out whether a message had been forgotten

Solution: get_messages now calls .../messages/{item_hash} endpoint and raises ForgottenMessageException
  • Loading branch information
MHHukiewitz authored Nov 22, 2023
1 parent 5e47d96 commit 59e22b5
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 110 deletions.
2 changes: 0 additions & 2 deletions src/aleph/sdk/client/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 13 additions & 14 deletions src/aleph/sdk/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions src/aleph/sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
91 changes: 91 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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: {
Expand All @@ -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
49 changes: 49 additions & 0 deletions tests/unit/post.json
Original file line number Diff line number Diff line change
@@ -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
}
51 changes: 2 additions & 49 deletions tests/unit/test_asynchronous.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
Expand Down
Loading

0 comments on commit 59e22b5

Please sign in to comment.