Skip to content

Commit 94c84dd

Browse files
committed
test(integration): close v3.0.0 coverage gaps; fix demo-drift error tests
Post-v3.0.0 release surfaced 5 failing integration tests (release was merged with bypassed status checks). All five are test maintenance, not SDK bugs: Coverage harness: - Expected resource set now includes v3 sub-resources RFQsResource and QuotesResource (21 classes, was 19). - Register PortfolioResource.{fills, fills_all, positions_all} — relocated from OrdersResource in #351/#376. - Register MarketsResource.list_all_trades (canonical v3 name). - Register FcmResource.positions_all (added in #344). - Register RFQsResource + QuotesResource public methods. Integration tests for v3 surfaces: - tests/integration/test_communications.py: add TestRFQsV3Sync, TestQuotesV3Sync, TestQuotesV3RealApiOnly — exercise the namespaced client.communications.{rfqs,quotes}.* surface end-to-end on demo. - tests/integration/test_portfolio.py: add fills/fills_all/positions_all smoke tests on both sync and async. - tests/integration/test_markets.py: add test_list_all_trades. - tests/integration/test_fcm.py: add positions_all sync + async. Demo-drift fixes in test_errors.py: - test_malformed_params_returns_validation_error + test_validation_error_details_attribute: demo now silently accepts malformed cursors. Switched to status='not-a-real-status' which reliably triggers 400 invalid_status_filter. - test_bad_auth_returns_auth_error: KalshiConfig(base_url=...) now rejects split REST/WS environments (defense-in-depth from #261). Switched to KalshiConfig.demo(). Verification: Unit: 2954 passed, 3 skipped Integration: 233 passed, 38 skipped (was 218 passed / 5 failed)
1 parent 1894a61 commit 94c84dd

6 files changed

Lines changed: 243 additions & 11 deletions

File tree

tests/integration/test_communications.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@
6464
"list_rfqs",
6565
],
6666
)
67+
register(
68+
"RFQsResource",
69+
["create", "delete", "get", "list", "list_all"],
70+
)
71+
register(
72+
"QuotesResource",
73+
["accept", "confirm", "create", "delete", "get", "list", "list_all"],
74+
)
6775

6876

6977
@pytest.fixture
@@ -179,6 +187,139 @@ def test_get_unknown_quote_errors(self, sync_client: KalshiClient) -> None:
179187
sync_client.communications.get_quote("q-nonexistent-00000000")
180188

181189

190+
@pytest.mark.integration
191+
class TestRFQsV3Sync:
192+
"""v3 namespaced surface: ``client.communications.rfqs.*``.
193+
194+
These calls forward to the same endpoints as the deprecated
195+
top-level methods; this class proves the new public path works
196+
end-to-end on demo.
197+
"""
198+
199+
def test_list(self, sync_client: KalshiClient) -> None:
200+
page = sync_client.communications.rfqs.list(limit=10)
201+
assert isinstance(page, Page)
202+
for rfq in page.items:
203+
assert isinstance(rfq, RFQ)
204+
205+
def test_list_all(self, sync_client: KalshiClient) -> None:
206+
for i, rfq in enumerate(
207+
sync_client.communications.rfqs.list_all(limit=10),
208+
):
209+
assert isinstance(rfq, RFQ)
210+
if i >= 4:
211+
break
212+
213+
def test_create_get_delete(
214+
self, sync_client: KalshiClient, demo_market_ticker: str,
215+
) -> None:
216+
try:
217+
created = sync_client.communications.rfqs.create(
218+
market_ticker=demo_market_ticker,
219+
rest_remainder=False,
220+
contracts=1,
221+
)
222+
except (KalshiValidationError, KalshiAuthError) as e:
223+
pytest.skip(f"demo refused rfqs.create: {e}")
224+
assert isinstance(created, CreateRFQResponse)
225+
try:
226+
time.sleep(0.5) # eventual consistency on demo
227+
got = sync_client.communications.rfqs.get(created.id)
228+
assert isinstance(got, GetRFQResponse)
229+
assert got.rfq.id == created.id
230+
finally:
231+
sync_client.communications.rfqs.delete(created.id)
232+
233+
234+
@pytest.mark.integration
235+
class TestQuotesV3Sync:
236+
"""v3 namespaced surface: ``client.communications.quotes.*``.
237+
238+
``list`` and ``list_all`` need a creator filter on demo; ``accept`` and
239+
``confirm`` require two parties — both live behind the real-API gate to
240+
mirror ``TestCommunicationsRealApiOnly``. The lifecycle test exercises
241+
``create`` / ``get`` / ``delete`` which the demo supports for self-quoting.
242+
"""
243+
244+
def test_quote_lifecycle(
245+
self, sync_client: KalshiClient, ephemeral_rfq: str,
246+
) -> None:
247+
try:
248+
created = sync_client.communications.quotes.create(
249+
rfq_id=ephemeral_rfq,
250+
yes_bid="0.50",
251+
no_bid="0.50",
252+
rest_remainder=False,
253+
)
254+
except (KalshiValidationError, KalshiAuthError) as e:
255+
pytest.skip(f"demo refused quotes.create: {e}")
256+
quote_id = created.id
257+
try:
258+
time.sleep(0.5)
259+
got = sync_client.communications.quotes.get(quote_id)
260+
assert got.quote.id == quote_id
261+
assert got.quote.rfq_id == ephemeral_rfq
262+
finally:
263+
try:
264+
sync_client.communications.quotes.delete(quote_id)
265+
except Exception:
266+
logger.warning("cleanup: failed to delete quote %s", quote_id)
267+
268+
269+
@pytest.mark.integration
270+
@pytest.mark.integration_real_api_only
271+
class TestQuotesV3RealApiOnly:
272+
"""v3 namespaced surface for endpoints the demo can't service.
273+
274+
See ``TestCommunicationsRealApiOnly`` for the underlying demo
275+
constraints — these tests mirror that gating against the new
276+
``client.communications.quotes`` namespace.
277+
"""
278+
279+
def test_list(self, sync_client: KalshiClient) -> None:
280+
comms_id = sync_client.communications.get_id().communications_id
281+
page = sync_client.communications.quotes.list(
282+
limit=10, quote_creator_user_id=comms_id,
283+
)
284+
assert isinstance(page, Page)
285+
286+
def test_list_all(self, sync_client: KalshiClient) -> None:
287+
comms_id = sync_client.communications.get_id().communications_id
288+
for i, quote in enumerate(
289+
sync_client.communications.quotes.list_all(
290+
limit=10, quote_creator_user_id=comms_id,
291+
),
292+
):
293+
assert isinstance(quote, Quote)
294+
if i >= 4:
295+
break
296+
297+
def test_accept_and_confirm(
298+
self, sync_client: KalshiClient, demo_market_ticker: str,
299+
) -> None:
300+
rfq = sync_client.communications.rfqs.create(
301+
market_ticker=demo_market_ticker,
302+
rest_remainder=False,
303+
contracts=1,
304+
)
305+
try:
306+
quote = sync_client.communications.quotes.create(
307+
rfq_id=rfq.id,
308+
yes_bid="0.50",
309+
no_bid="0.50",
310+
rest_remainder=False,
311+
)
312+
sync_client.communications.quotes.accept(
313+
quote.id, accepted_side="yes",
314+
)
315+
sync_client.communications.quotes.confirm(quote.id)
316+
finally:
317+
try:
318+
sync_client.communications.rfqs.delete(rfq.id)
319+
except Exception:
320+
logger.warning("cleanup: failed to delete rfq %s", rfq.id)
321+
322+
182323
@pytest.mark.integration
183324
@pytest.mark.integration_real_api_only
184325
class TestCommunicationsRealApiOnly:

tests/integration/test_coverage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_no_stale_registrations(self) -> None:
6969
)
7070

7171
def test_discovery_finds_all_resources(self) -> None:
72-
"""Sanity check: discover finds the expected 19 resource classes."""
72+
"""Sanity check: discover finds the expected 21 resource classes."""
7373
discovered = discover_public_methods()
7474
expected = {
7575
"AccountResource",
@@ -86,6 +86,8 @@ def test_discovery_finds_all_resources(self) -> None:
8686
"ExchangeResource",
8787
"HistoricalResource",
8888
"PortfolioResource",
89+
"QuotesResource",
90+
"RFQsResource",
8991
"SearchResource",
9092
"SeriesResource",
9193
"StructuredTargetsResource",

tests/integration/test_errors.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ def test_malformed_params_returns_validation_error(
4242
) -> None:
4343
"""Malformed request params should raise KalshiValidationError (400).
4444
45-
Uses a malformed cursor — the SDK passes it verbatim to the server,
46-
which rejects it with a 400. (Numeric range checks like negative
47-
limit are now enforced client-side via ValueError per #214.)
45+
Uses an invalid ``status`` filter — the server rejects values outside
46+
its accepted enum with 400 invalid_status_filter. (Numeric range
47+
checks like negative ``limit`` are enforced client-side via
48+
``ValueError`` per #214, and malformed cursors are now silently
49+
ignored by the demo server.)
4850
"""
4951
with pytest.raises(KalshiValidationError) as exc_info:
50-
sync_client.markets.list(cursor="!!!not-a-valid-cursor!!!")
52+
sync_client.markets.list(status="not-a-real-status") # type: ignore[arg-type]
5153

5254
exc = exc_info.value
5355
assert exc.status_code == 400
@@ -70,9 +72,7 @@ def test_bad_auth_returns_auth_error(self) -> None:
7072
auth = KalshiAuth(
7173
key_id="invalid-key-id-for-test", private_key=dummy_key
7274
)
73-
config = KalshiConfig(
74-
base_url="https://demo-api.kalshi.co/trade-api/v2"
75-
)
75+
config = KalshiConfig.demo()
7676
client = KalshiClient(auth=auth, config=config)
7777

7878
try:
@@ -102,7 +102,7 @@ def test_validation_error_details_attribute(
102102
) -> None:
103103
"""KalshiValidationError should have a details attribute (may be None or dict)."""
104104
with pytest.raises(KalshiValidationError) as exc_info:
105-
sync_client.markets.list(cursor="!!!not-a-valid-cursor!!!")
105+
sync_client.markets.list(status="not-a-real-status") # type: ignore[arg-type]
106106

107107
exc = exc_info.value
108108
assert hasattr(exc, "details")

tests/integration/test_fcm.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from kalshi.client import KalshiClient
1414
from kalshi.errors import KalshiAuthError, KalshiNotFoundError, KalshiServerError
1515
from kalshi.models.orders import Order
16-
from kalshi.models.portfolio import PositionsResponse
16+
from kalshi.models.portfolio import MarketPosition, PositionsResponse
1717
from tests.integration.assertions import assert_model_fields
1818
from tests.integration.coverage_harness import register
1919

20-
register("FcmResource", ["orders", "orders_all", "positions"])
20+
register("FcmResource", ["orders", "orders_all", "positions", "positions_all"])
2121

2222
# Tolerated errors on demo for non-FCM accounts:
2323
# - 401/403 → KalshiAuthError (expected: demo account lacks FCM role)
@@ -65,6 +65,19 @@ def test_positions(self, sync_client: KalshiClient) -> None:
6565
assert isinstance(result, PositionsResponse)
6666
assert_model_fields(result)
6767

68+
def test_positions_all(self, sync_client: KalshiClient) -> None:
69+
try:
70+
gen = sync_client.fcm.positions_all(
71+
subtrader_id="sdk-test-subtrader",
72+
limit=2,
73+
)
74+
for count, pos in enumerate(gen):
75+
assert isinstance(pos, MarketPosition)
76+
if count >= 2:
77+
break
78+
except _TOLERATED_FCM_ERRORS:
79+
pytest.skip("Demo account is not FCM-enabled (expected)")
80+
6881

6982
@pytest.mark.integration
7083
class TestFcmAsync:
@@ -85,3 +98,18 @@ async def test_positions(self, async_client: AsyncKalshiClient) -> None:
8598
except _TOLERATED_FCM_ERRORS:
8699
pytest.skip("Demo account is not FCM-enabled (expected)")
87100
assert isinstance(result, PositionsResponse)
101+
102+
async def test_positions_all(self, async_client: AsyncKalshiClient) -> None:
103+
try:
104+
gen = async_client.fcm.positions_all(
105+
subtrader_id="sdk-test-subtrader",
106+
limit=2,
107+
)
108+
count = 0
109+
async for pos in gen:
110+
assert isinstance(pos, MarketPosition)
111+
count += 1
112+
if count >= 2:
113+
break
114+
except _TOLERATED_FCM_ERRORS:
115+
pytest.skip("Demo account is not FCM-enabled (expected)")

tests/integration/test_markets.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"get",
3131
"list",
3232
"list_all",
33+
"list_all_trades",
3334
"list_trades",
3435
"list_trades_all",
3536
"orderbook",
@@ -180,6 +181,15 @@ def test_list_trades_all(self, sync_client: KalshiClient) -> None:
180181
if count >= 2:
181182
break
182183

184+
def test_list_all_trades(self, sync_client: KalshiClient) -> None:
185+
# v3 canonical name; ``list_trades_all`` is a deprecated forwarder.
186+
for count, t in enumerate(
187+
sync_client.markets.list_all_trades(limit=5),
188+
):
189+
assert isinstance(t, Trade)
190+
if count >= 2:
191+
break
192+
183193
def test_bulk_candlesticks(
184194
self,
185195
sync_client: KalshiClient,

tests/integration/test_portfolio.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from kalshi.async_client import AsyncKalshiClient
88
from kalshi.client import KalshiClient
99
from kalshi.models.common import Page
10+
from kalshi.models.orders import Fill
1011
from kalshi.models.portfolio import (
1112
Balance,
1213
Deposit,
14+
MarketPosition,
1315
PositionsResponse,
1416
Settlement,
1517
TotalRestingOrderValue,
@@ -24,7 +26,10 @@
2426
"balance",
2527
"deposits",
2628
"deposits_all",
29+
"fills",
30+
"fills_all",
2731
"positions",
32+
"positions_all",
2833
"settlements",
2934
"settlements_all",
3035
"total_resting_order_value",
@@ -51,6 +56,27 @@ def test_positions(self, sync_client: KalshiClient) -> None:
5156
assert isinstance(result.market_positions, list)
5257
assert isinstance(result.event_positions, list)
5358

59+
def test_positions_all(self, sync_client: KalshiClient) -> None:
60+
for count, pos in enumerate(sync_client.portfolio.positions_all(limit=2)):
61+
assert isinstance(pos, MarketPosition)
62+
assert_model_fields(pos)
63+
if count >= 2:
64+
break
65+
66+
def test_fills(self, sync_client: KalshiClient) -> None:
67+
page = sync_client.portfolio.fills(limit=5)
68+
assert isinstance(page, Page)
69+
for item in page.items:
70+
assert isinstance(item, Fill)
71+
assert_model_fields(item)
72+
73+
def test_fills_all(self, sync_client: KalshiClient) -> None:
74+
for count, fill in enumerate(sync_client.portfolio.fills_all(limit=2)):
75+
assert isinstance(fill, Fill)
76+
assert_model_fields(fill)
77+
if count >= 2:
78+
break
79+
5480
def test_settlements(self, sync_client: KalshiClient) -> None:
5581
page = sync_client.portfolio.settlements(limit=5)
5682
assert isinstance(page, Page)
@@ -115,6 +141,31 @@ async def test_positions(self, async_client: AsyncKalshiClient) -> None:
115141
assert isinstance(result, PositionsResponse)
116142
assert_model_fields(result)
117143

144+
async def test_positions_all(self, async_client: AsyncKalshiClient) -> None:
145+
count = 0
146+
async for pos in async_client.portfolio.positions_all(limit=2):
147+
assert isinstance(pos, MarketPosition)
148+
assert_model_fields(pos)
149+
count += 1
150+
if count >= 3:
151+
break
152+
153+
async def test_fills(self, async_client: AsyncKalshiClient) -> None:
154+
page = await async_client.portfolio.fills(limit=5)
155+
assert isinstance(page, Page)
156+
for item in page.items:
157+
assert isinstance(item, Fill)
158+
assert_model_fields(item)
159+
160+
async def test_fills_all(self, async_client: AsyncKalshiClient) -> None:
161+
count = 0
162+
async for fill in async_client.portfolio.fills_all(limit=2):
163+
assert isinstance(fill, Fill)
164+
assert_model_fields(fill)
165+
count += 1
166+
if count >= 3:
167+
break
168+
118169
async def test_settlements(self, async_client: AsyncKalshiClient) -> None:
119170
page = await async_client.portfolio.settlements(limit=5)
120171
assert isinstance(page, Page)

0 commit comments

Comments
 (0)