Skip to content

Commit 58ee37a

Browse files
authored
Add integration tests for IPFSClient and ExchangeClient (#57)
1 parent 4513deb commit 58ee37a

File tree

3 files changed

+1418
-7
lines changed

3 files changed

+1418
-7
lines changed

tests/fixtures.py

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
"""Shared test fixtures and helpers for integration tests."""
2+
3+
from datetime import datetime, timezone
4+
from decimal import Decimal
5+
from typing import Any
6+
from unittest.mock import Mock
7+
8+
import ipld_car # type: ignore (untyped library)
9+
from requests import Response
10+
11+
from afp.dtos import (
12+
ComponentLink,
13+
ExchangeParameters,
14+
ExchangeProductListingSubmission,
15+
ExchangeProductUpdateSubmission,
16+
ExtendedMetadata,
17+
ExtendedMetadataDAG,
18+
LoginSubmission,
19+
OrderSubmission,
20+
)
21+
from afp.enums import ListingState, OrderSide, OrderState, OrderType, TradeState
22+
from afp.schemas import (
23+
ExchangeProduct,
24+
IntentData,
25+
MarketDepthData,
26+
MarketDepthItem,
27+
OHLCVItem,
28+
OracleFallback,
29+
Order,
30+
OrderFill,
31+
OutcomePointTimeSeries,
32+
OutcomeSpaceTimeSeries,
33+
Trade,
34+
)
35+
36+
37+
# Sample data factories for DTOs
38+
39+
40+
def make_exchange_product(**overrides: Any) -> ExchangeProduct:
41+
"""Create a sample ExchangeProduct with optional field overrides."""
42+
defaults = {
43+
"id": "0x1234567890123456789012345678901234567890123456789012345678901234",
44+
"symbol": "BTCUSD",
45+
"tick_size": 1,
46+
"collateral_asset": "0x0000000000000000000000000000000000000001",
47+
"listing_state": ListingState.PUBLIC,
48+
"min_price": Decimal("0"),
49+
"max_price": Decimal("100000"),
50+
}
51+
return ExchangeProduct(**{**defaults, **overrides})
52+
53+
54+
def make_intent_data(**overrides: Any) -> IntentData:
55+
"""Create a sample IntentData with optional field overrides."""
56+
defaults = {
57+
"trading_protocol_id": "0xabcd",
58+
"product_id": "0x1234567890123456789012345678901234567890123456789012345678901234",
59+
"limit_price": Decimal("50000"),
60+
"quantity": 100,
61+
"max_trading_fee_rate": Decimal("0.001"),
62+
"side": OrderSide.BID,
63+
"good_until_time": 1700000000,
64+
"nonce": 1,
65+
"referral": "0x0000000000000000000000000000000000000000",
66+
}
67+
return IntentData(**{**defaults, **overrides})
68+
69+
70+
def make_order(**overrides: Any) -> Order:
71+
"""Create a sample Order with optional field overrides."""
72+
intent_data = make_intent_data()
73+
defaults = {
74+
"id": "order-123",
75+
"type": OrderType.LIMIT_ORDER,
76+
"timestamp": 1700000000,
77+
"state": OrderState.OPEN,
78+
"fill_quantity": 0,
79+
"intent": {
80+
"hash": "0x9876543210987654321098765432109876543210987654321098765432109876",
81+
"margin_account_id": "0x1111111111111111111111111111111111111111",
82+
"intent_account_id": "0x2222222222222222222222222222222222222222",
83+
"signature": "0xabcdef",
84+
"data": intent_data.model_dump(),
85+
},
86+
}
87+
return Order(**{**defaults, **overrides})
88+
89+
90+
def make_trade(**overrides: Any) -> Trade:
91+
"""Create a sample Trade with optional field overrides."""
92+
defaults = {
93+
"id": "123",
94+
"product_id": "0x1234567890123456789012345678901234567890123456789012345678901234",
95+
"price": Decimal("50000"),
96+
"timestamp": 1700000000,
97+
"state": TradeState.CLEARED,
98+
"transaction_id": "0xabc123",
99+
"rejection_reason": None,
100+
}
101+
return Trade(**{**defaults, **overrides})
102+
103+
104+
def make_order_fill(**overrides: Any) -> OrderFill:
105+
"""Create a sample OrderFill with optional field overrides."""
106+
defaults = {
107+
"order": make_order().model_dump(),
108+
"trade": make_trade().model_dump(),
109+
"quantity": 50,
110+
"price": Decimal("50000"),
111+
"trading_fee_rate": Decimal("0.001"),
112+
}
113+
return OrderFill(**{**defaults, **overrides})
114+
115+
116+
def make_market_depth_item(**overrides: Any) -> MarketDepthItem:
117+
"""Create a sample MarketDepthItem with optional field overrides."""
118+
defaults = {
119+
"price": Decimal("50000"),
120+
"quantity": 100,
121+
}
122+
return MarketDepthItem(**{**defaults, **overrides})
123+
124+
125+
def make_market_depth_data(**overrides: Any) -> MarketDepthData:
126+
"""Create a sample MarketDepthData with optional field overrides."""
127+
defaults = {
128+
"product_id": "0x1234567890123456789012345678901234567890123456789012345678901234",
129+
"bids": [
130+
make_market_depth_item(price=Decimal("49900"), quantity=50).model_dump(),
131+
make_market_depth_item(price=Decimal("49800"), quantity=100).model_dump(),
132+
],
133+
"asks": [
134+
make_market_depth_item(price=Decimal("50100"), quantity=50).model_dump(),
135+
make_market_depth_item(price=Decimal("50200"), quantity=100).model_dump(),
136+
],
137+
}
138+
return MarketDepthData(**{**defaults, **overrides})
139+
140+
141+
def make_ohlcv_item(**overrides: Any) -> OHLCVItem:
142+
"""Create a sample OHLCVItem with optional field overrides."""
143+
defaults = {
144+
"timestamp": 1700000000,
145+
"open": Decimal("50000"),
146+
"high": Decimal("51000"),
147+
"low": Decimal("49000"),
148+
"close": Decimal("50500"),
149+
"volume": 1000,
150+
}
151+
return OHLCVItem(**{**defaults, **overrides})
152+
153+
154+
def make_login_submission(**overrides: Any) -> LoginSubmission:
155+
"""Create a sample LoginSubmission with optional field overrides."""
156+
defaults = {
157+
"message": "Login message",
158+
"signature": "0xabcdef1234567890",
159+
}
160+
return LoginSubmission(**{**defaults, **overrides})
161+
162+
163+
def make_exchange_parameters(**overrides: Any) -> ExchangeParameters:
164+
"""Create a sample ExchangeParameters with optional field overrides."""
165+
defaults = {
166+
"trading_protocol_id": "0xabcd",
167+
"maker_trading_fee_rate": Decimal("0.001"),
168+
"taker_trading_fee_rate": Decimal("0.002"),
169+
}
170+
return ExchangeParameters(**{**defaults, **overrides})
171+
172+
173+
def make_order_submission(**overrides: Any) -> OrderSubmission:
174+
"""Create a sample OrderSubmission with optional field overrides."""
175+
intent_data = make_intent_data()
176+
defaults = {
177+
"type": OrderType.LIMIT_ORDER,
178+
"intent": {
179+
"hash": "0x9876543210987654321098765432109876543210987654321098765432109876",
180+
"margin_account_id": "0x1111111111111111111111111111111111111111",
181+
"intent_account_id": "0x2222222222222222222222222222222222222222",
182+
"signature": "0xabcdef",
183+
"data": intent_data.model_dump(),
184+
},
185+
"cancellation_data": None,
186+
}
187+
return OrderSubmission(**{**defaults, **overrides})
188+
189+
190+
def make_product_listing_submission(
191+
**overrides: Any,
192+
) -> ExchangeProductListingSubmission:
193+
"""Create a sample ExchangeProductListingSubmission."""
194+
defaults = {
195+
"id": "0x1234567890123456789012345678901234567890123456789012345678901234",
196+
}
197+
return ExchangeProductListingSubmission(**{**defaults, **overrides})
198+
199+
200+
def make_product_update_submission(**overrides: Any) -> ExchangeProductUpdateSubmission:
201+
"""Create a sample ExchangeProductUpdateSubmission."""
202+
defaults = {
203+
"listing_state": ListingState.PUBLIC,
204+
}
205+
return ExchangeProductUpdateSubmission(**{**defaults, **overrides})
206+
207+
208+
# IPFS-specific fixtures
209+
210+
211+
def make_outcome_space_time_series(**overrides: Any) -> OutcomeSpaceTimeSeries:
212+
"""Create a sample OutcomeSpaceTimeSeries with optional field overrides."""
213+
defaults = {
214+
"fsp_type": "scalar",
215+
"description": "Test outcome space",
216+
"base_case": {
217+
"condition": "value is valid",
218+
"fsp_resolution": "value",
219+
},
220+
"edge_cases": [],
221+
"units": "USD",
222+
"source_name": "Test Source",
223+
"source_uri": "https://example.com/data",
224+
"frequency": "daily",
225+
"history_api_spec": None,
226+
}
227+
return OutcomeSpaceTimeSeries(**{**defaults, **overrides})
228+
229+
230+
def make_outcome_point_time_series(**overrides: Any) -> OutcomePointTimeSeries:
231+
"""Create a sample OutcomePointTimeSeries with optional field overrides."""
232+
defaults = {
233+
"fsp_type": "scalar",
234+
"observation": {
235+
"reference_date": "2024-01-01",
236+
"release_date": "2024-01-02",
237+
},
238+
}
239+
return OutcomePointTimeSeries(**{**defaults, **overrides})
240+
241+
242+
def make_oracle_fallback(**overrides: Any) -> OracleFallback:
243+
"""Create a sample OracleFallback with optional field overrides."""
244+
defaults = {
245+
"fallback_time": datetime(2025, 1, 15, tzinfo=timezone.utc),
246+
"fallback_fsp": Decimal("100"),
247+
}
248+
return OracleFallback(**{**defaults, **overrides})
249+
250+
251+
def make_extended_metadata(**overrides: Any) -> ExtendedMetadata:
252+
"""Create a sample ExtendedMetadata with optional field overrides."""
253+
from afp.schemas import OracleConfig
254+
255+
defaults = {
256+
"outcome_space": make_outcome_space_time_series(),
257+
"outcome_point": make_outcome_point_time_series(),
258+
"oracle_config": OracleConfig(
259+
description="Test oracle",
260+
project_url="https://example.com",
261+
),
262+
"oracle_fallback": make_oracle_fallback(),
263+
}
264+
return ExtendedMetadata(**{**defaults, **overrides})
265+
266+
267+
def make_extended_metadata_dag(**overrides: Any) -> ExtendedMetadataDAG:
268+
"""Create a sample ExtendedMetadataDAG with optional field overrides."""
269+
# Use real valid CIDs (minimum valid base32 CIDs)
270+
defaults = {
271+
"outcome_space": ComponentLink(
272+
data="bafyreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
273+
schema_="bafyreibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
274+
).model_dump(),
275+
"outcome_point": ComponentLink(
276+
data="bafyreiccccccccccccccccccccccccccccccccccccccccccccccccccccc",
277+
schema_="bafyreiddddddddddddddddddddddddddddddddddddddddddddddddddddd",
278+
).model_dump(),
279+
"oracle_config": ComponentLink(
280+
data="bafyreieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
281+
schema_="bafyreifffffffffffffffffffffffffffffffffffffffffffffffffffffff",
282+
).model_dump(),
283+
"oracle_fallback": ComponentLink(
284+
data="bafyreiggggggggggggggggggggggggggggggggggggggggggggggggggggg",
285+
schema_="bafyreihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh",
286+
).model_dump(),
287+
}
288+
return ExtendedMetadataDAG(**{**defaults, **overrides})
289+
290+
291+
# Response generators
292+
293+
294+
def make_ndjson_response(items: list[Any]) -> Response:
295+
"""Create a mock Response with iter_lines() returning NDJSON."""
296+
mock_response = Response()
297+
mock_response.status_code = 200
298+
mock_response._content = b"" # type: ignore
299+
mock_response.iter_lines = Mock( # type: ignore
300+
return_value=iter([item.model_dump_json().encode() for item in items])
301+
)
302+
return mock_response
303+
304+
305+
def make_error_response(status_code: int, detail: str | list[Any]) -> Response:
306+
"""Generate standardized error responses for various HTTP codes."""
307+
import requests
308+
309+
mock_response = Response()
310+
mock_response.status_code = status_code
311+
mock_response._content = b"" # type: ignore
312+
313+
if isinstance(detail, str):
314+
mock_response.json = Mock(return_value={"detail": detail}) # type: ignore
315+
else:
316+
mock_response.json = Mock(return_value={"detail": detail}) # type: ignore
317+
318+
# Simulate raise_for_status behavior
319+
http_error = requests.exceptions.HTTPError(response=mock_response)
320+
mock_response.raise_for_status = Mock(side_effect=http_error) # type: ignore
321+
322+
return mock_response
323+
324+
325+
def make_success_response(data: dict[str, Any]) -> Response:
326+
"""Create a mock successful Response."""
327+
mock_response = Response()
328+
mock_response.status_code = 200
329+
mock_response._content = b"" # type: ignore
330+
mock_response.json = Mock(return_value=data) # type: ignore
331+
mock_response.history = [] # type: ignore
332+
return mock_response
333+
334+
335+
# IPFS-specific helpers
336+
337+
338+
def make_ipfs_car_response(root_cid: str) -> Response:
339+
"""Create a mock IPFS CAR upload response."""
340+
mock_response = Response()
341+
mock_response.status_code = 200
342+
mock_response._content = b"" # type: ignore
343+
mock_response.json = Mock( # type: ignore
344+
return_value={
345+
"Root": {
346+
"Cid": {"/": root_cid},
347+
},
348+
}
349+
)
350+
return mock_response
351+
352+
353+
def make_ipfs_block_response(content: bytes) -> Response:
354+
"""Create a mock IPFS block download response."""
355+
mock_response = Response()
356+
mock_response.status_code = 200
357+
mock_response._content = content # type: ignore
358+
return mock_response
359+
360+
361+
def make_car_bytes(blocks: list[ipld_car.Block]) -> bytes:
362+
"""Generate CAR file bytes from blocks."""
363+
root_cid = blocks[0][0]
364+
return ipld_car.encode([root_cid], blocks).tobytes()

0 commit comments

Comments
 (0)