Skip to content

Commit 59e22b5

Browse files
authored
Refactor: Simplify and improve get_messages; use mocked responses (#81)
* 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
1 parent 5e47d96 commit 59e22b5

File tree

7 files changed

+190
-110
lines changed

7 files changed

+190
-110
lines changed

src/aleph/sdk/client/abstract.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,12 @@ async def get_message(
195195
self,
196196
item_hash: str,
197197
message_type: Optional[Type[GenericMessage]] = None,
198-
channel: Optional[str] = None,
199198
) -> GenericMessage:
200199
"""
201200
Get a single message from its `item_hash` and perform some basic validation.
202201
203202
:param item_hash: Hash of the message to fetch
204203
:param message_type: Type of message to fetch
205-
:param channel: Channel of the message to fetch
206204
"""
207205
pass
208206

src/aleph/sdk/client/http.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic import ValidationError
1010

1111
from ..conf import settings
12-
from ..exceptions import FileTooLarge, MessageNotFoundError, MultipleMessagesError
12+
from ..exceptions import FileTooLarge, ForgottenMessageError, MessageNotFoundError
1313
from ..query.filters import MessageFilter, PostFilter
1414
from ..query.responses import MessagesResponse, Post, PostsResponse
1515
from ..types import GenericMessage
@@ -290,21 +290,20 @@ async def get_message(
290290
self,
291291
item_hash: str,
292292
message_type: Optional[Type[GenericMessage]] = None,
293-
channel: Optional[str] = None,
294293
) -> GenericMessage:
295-
messages_response = await self.get_messages(
296-
message_filter=MessageFilter(
297-
hashes=[item_hash],
298-
channels=[channel] if channel else None,
294+
async with self.http_session.get(f"/api/v0/messages/{item_hash}") as resp:
295+
try:
296+
resp.raise_for_status()
297+
except aiohttp.ClientResponseError as e:
298+
if e.status == 404:
299+
raise MessageNotFoundError(f"No such hash {item_hash}")
300+
raise e
301+
message_raw = await resp.json()
302+
if message_raw["status"] == "forgotten":
303+
raise ForgottenMessageError(
304+
f"The requested message {message_raw['item_hash']} has been forgotten by {', '.join(message_raw['forgotten_by'])}"
299305
)
300-
)
301-
if len(messages_response.messages) < 1:
302-
raise MessageNotFoundError(f"No such hash {item_hash}")
303-
if len(messages_response.messages) != 1:
304-
raise MultipleMessagesError(
305-
f"Multiple messages found for the same item_hash `{item_hash}`"
306-
)
307-
message: GenericMessage = messages_response.messages[0]
306+
message = parse_message(message_raw["message"])
308307
if message_type:
309308
expected_type = get_message_type_value(message_type)
310309
if message.type != expected_type:

src/aleph/sdk/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,9 @@ class DomainConfigurationError(Exception):
5656
"""Raised when the domain checks are not satisfied"""
5757

5858
pass
59+
60+
61+
class ForgottenMessageError(QueryError):
62+
"""The requested message was forgotten"""
63+
64+
pass

tests/unit/conftest.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from tempfile import NamedTemporaryFile
44
from typing import Any, Callable, Dict, List
5+
from unittest.mock import AsyncMock, MagicMock
56

67
import pytest as pytest
78
from aleph_message.models import AggregateMessage, AlephMessage, PostMessage
@@ -10,7 +11,9 @@
1011
import aleph.sdk.chains.sol as solana
1112
import aleph.sdk.chains.substrate as substrate
1213
import aleph.sdk.chains.tezos as tezos
14+
from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient
1315
from aleph.sdk.chains.common import get_fallback_private_key
16+
from aleph.sdk.types import Account
1417

1518

1619
@pytest.fixture
@@ -111,6 +114,12 @@ def aleph_messages() -> List[AlephMessage]:
111114
]
112115

113116

117+
@pytest.fixture
118+
def json_post() -> dict:
119+
with open(Path(__file__).parent / "post.json", "r") as f:
120+
return json.load(f)
121+
122+
114123
@pytest.fixture
115124
def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]:
116125
return lambda page: {
@@ -122,3 +131,85 @@ def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]:
122131
"pagination_per_page": max(len(aleph_messages), 20),
123132
"pagination_total": len(aleph_messages) if page == 1 else 0,
124133
}
134+
135+
136+
@pytest.fixture
137+
def raw_posts_response(json_post) -> Callable[[int], Dict[str, Any]]:
138+
return lambda page: {
139+
"posts": [json_post] if int(page) == 1 else [],
140+
"pagination_item": "posts",
141+
"pagination_page": int(page),
142+
"pagination_per_page": 1,
143+
"pagination_total": 1 if page == 1 else 0,
144+
}
145+
146+
147+
class MockResponse:
148+
def __init__(self, sync: bool):
149+
self.sync = sync
150+
151+
async def __aenter__(self):
152+
return self
153+
154+
async def __aexit__(self, exc_type, exc_val, exc_tb):
155+
...
156+
157+
async def raise_for_status(self):
158+
...
159+
160+
@property
161+
def status(self):
162+
return 200 if self.sync else 202
163+
164+
async def json(self):
165+
message_status = "processed" if self.sync else "pending"
166+
return {
167+
"message_status": message_status,
168+
"publication_status": {"status": "success", "failed": []},
169+
}
170+
171+
async def text(self):
172+
return json.dumps(await self.json())
173+
174+
175+
@pytest.fixture
176+
def mock_session_with_post_success(
177+
ethereum_account: Account,
178+
) -> AuthenticatedAlephHttpClient:
179+
http_session = AsyncMock()
180+
http_session.post = MagicMock()
181+
http_session.post.side_effect = lambda *args, **kwargs: MockResponse(
182+
sync=kwargs.get("sync", False)
183+
)
184+
185+
client = AuthenticatedAlephHttpClient(
186+
account=ethereum_account, api_server="http://localhost"
187+
)
188+
client.http_session = http_session
189+
190+
return client
191+
192+
193+
def make_custom_mock_response(resp_json, status=200) -> MockResponse:
194+
class CustomMockResponse(MockResponse):
195+
async def json(self):
196+
return resp_json
197+
198+
@property
199+
def status(self):
200+
return status
201+
202+
return CustomMockResponse(sync=True)
203+
204+
205+
def make_mock_get_session(get_return_value: Dict[str, Any]) -> AlephHttpClient:
206+
class MockHttpSession(AsyncMock):
207+
def get(self, *_args, **_kwargs):
208+
return make_custom_mock_response(get_return_value)
209+
210+
http_session = MockHttpSession()
211+
212+
client = AlephHttpClient(api_server="http://localhost")
213+
client.http_session = http_session
214+
215+
return client

tests/unit/post.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"chain": "ETH",
3+
"item_hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe",
4+
"sender": "0x4D52380D3191274a04846c89c069E6C3F2Ed94e4",
5+
"type": "aleph-network-metrics",
6+
"channel": "aleph-scoring",
7+
"confirmed": false,
8+
"content": {
9+
"tags": [
10+
"mainnet"
11+
],
12+
"metrics": {
13+
"ccn": [
14+
{
15+
"asn": 24940,
16+
"url": "http://135.181.165.203:4024/",
17+
"as_name": "HETZNER-AS, DE",
18+
"node_id": "599de3dc1857b73d33bf616ab2f449df579e2f1270c9b04dc7bdc630524e1e6c",
19+
"version": "v0.5.1",
20+
"txs_total": 0,
21+
"measured_at": 1700562026.269039,
22+
"base_latency": 0.09740376472473145,
23+
"metrics_latency": 0.3925642967224121,
24+
"pending_messages": 0,
25+
"aggregate_latency": 0.06854844093322754,
26+
"base_latency_ipv4": 0.09740376472473145,
27+
"eth_height_remaining": 0,
28+
"file_download_latency": 0.10360932350158691
29+
}
30+
],
31+
"server": "151.115.63.76",
32+
"server_asn": 12876,
33+
"server_as_name": "Online SAS, FR"
34+
},
35+
"version": "1.0"
36+
},
37+
"item_content": null,
38+
"item_type": "storage",
39+
"signature": "0xc38c0ca2d683b2d0c629a640c156fbbce771c1d58d4c6f266bfa234f68b93302021981a9905d768510fb7fee050b6d5e48096258a2fec2aa531cc7594a4ede3e1b",
40+
"size": 125810,
41+
"time": 1700562222.942672,
42+
"confirmations": [],
43+
"original_item_hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe",
44+
"original_signature": "0xc38c0ca2d683b2d0c629a640c156fbbce771c1d58d4c6f266bfa234f68b93302021981a9905d768510fb7fee050b6d5e48096258a2fec2aa531cc7594a4ede3e1b",
45+
"original_type": "aleph-network-metrics",
46+
"hash": "b917624e649b632232879c657891e02b09b07298f0f67430753d89acf7489ebe",
47+
"address": "0x4D52380D3191274a04846c89c069E6C3F2Ed94e4",
48+
"ref": null
49+
}

tests/unit/test_asynchronous.py

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import json
2-
from unittest.mock import AsyncMock, MagicMock
1+
from unittest.mock import AsyncMock
32

43
import pytest as pytest
54
from aleph_message.models import (
@@ -12,53 +11,7 @@
1211
)
1312
from aleph_message.status import MessageStatus
1413

15-
from aleph.sdk.client import AuthenticatedAlephHttpClient
16-
from aleph.sdk.types import Account, StorageEnum
17-
18-
19-
@pytest.fixture
20-
def mock_session_with_post_success(
21-
ethereum_account: Account,
22-
) -> AuthenticatedAlephHttpClient:
23-
class MockResponse:
24-
def __init__(self, sync: bool):
25-
self.sync = sync
26-
27-
async def __aenter__(self):
28-
return self
29-
30-
async def __aexit__(self, exc_type, exc_val, exc_tb):
31-
...
32-
33-
@property
34-
def status(self):
35-
return 200 if self.sync else 202
36-
37-
async def raise_for_status(self):
38-
...
39-
40-
async def json(self):
41-
message_status = "processed" if self.sync else "pending"
42-
return {
43-
"message_status": message_status,
44-
"publication_status": {"status": "success", "failed": []},
45-
}
46-
47-
async def text(self):
48-
return json.dumps(await self.json())
49-
50-
http_session = AsyncMock()
51-
http_session.post = MagicMock()
52-
http_session.post.side_effect = lambda *args, **kwargs: MockResponse(
53-
sync=kwargs.get("sync", False)
54-
)
55-
56-
client = AuthenticatedAlephHttpClient(
57-
account=ethereum_account, api_server="http://localhost"
58-
)
59-
client.http_session = http_session
60-
61-
return client
14+
from aleph.sdk.types import StorageEnum
6215

6316

6417
@pytest.mark.asyncio

0 commit comments

Comments
 (0)