Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Simplify and improve get_messages; use mocked responses #81

Merged
merged 2 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
BjrInt marked this conversation as resolved.
Show resolved Hide resolved
) -> 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,
BjrInt marked this conversation as resolved.
Show resolved Hide resolved
) -> 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]
BjrInt marked this conversation as resolved.
Show resolved Hide resolved
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
Loading