Skip to content

Commit f53a886

Browse files
committed
test: restore 100% coverage after search and web changes
- Add tests for /public-search edge cases (malformed, no condition_id, empty) - Add tests for get_categories API and web route - Add tests for web command --reload flag - Add tests for _serialize list/dict branches and _trade_to_dict - Mark app.py main() as pragma: no cover (standalone entry point)
1 parent 2d92ea0 commit f53a886

5 files changed

Lines changed: 144 additions & 1 deletion

File tree

pm_trader/web/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def health() -> dict:
7272
return app
7373

7474

75-
def main() -> None:
75+
def main() -> None: # pragma: no cover
7676
import uvicorn
7777

7878
uvicorn.run(create_app(), host="127.0.0.1", port=8000)

tests/test_api.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,74 @@ def test_empty_events_returns_empty(
695695
result = client.search_markets("bitcoin")
696696
assert result == []
697697

698+
def test_malformed_market_skipped(
699+
self, client: PolymarketClient, httpx_mock
700+
):
701+
"""Markets that fail to parse are skipped (KeyError/TypeError)."""
702+
# outcomes as an int triggers TypeError in json.loads / iteration
703+
bad_market = {"condition_id": "0xbad", "outcomes": 999, "outcomePrices": 999}
704+
httpx_mock.add_response(json={
705+
"events": [{"markets": [bad_market, SAMPLE_GAMMA_MARKET]}],
706+
"pagination": {},
707+
})
708+
result = client.search_markets("bitcoin")
709+
assert len(result) == 1
710+
assert result[0].slug == "will-bitcoin-hit-100k"
711+
712+
def test_market_without_condition_id_skipped(
713+
self, client: PolymarketClient, httpx_mock
714+
):
715+
"""Markets without condition_id are skipped."""
716+
no_cid = {**SAMPLE_GAMMA_MARKET, "condition_id": "", "conditionId": ""}
717+
httpx_mock.add_response(json={
718+
"events": [{"markets": [no_cid]}],
719+
"pagination": {},
720+
})
721+
result = client.search_markets("bitcoin")
722+
assert result == []
723+
724+
725+
# ---------------------------------------------------------------------------
726+
# get_categories tests
727+
# ---------------------------------------------------------------------------
728+
729+
class TestGetCategories:
730+
def test_get_categories(self, client: PolymarketClient, httpx_mock):
731+
httpx_mock.add_response(json=[
732+
{"label": "Politics", "slug": "politics"},
733+
{"label": "Sports", "slug": "sports", "parentCategory": "top"},
734+
])
735+
result = client.get_categories(top_level_only=True)
736+
assert len(result) == 1
737+
assert result[0]["label"] == "Politics"
738+
739+
def test_get_categories_all(self, client: PolymarketClient, httpx_mock):
740+
httpx_mock.add_response(json=[
741+
{"label": "Politics", "slug": "politics"},
742+
{"label": "Sports", "slug": "sports", "parentCategory": "top"},
743+
])
744+
result = client.get_categories(top_level_only=False)
745+
assert len(result) == 2
746+
747+
def test_get_categories_empty(self, client: PolymarketClient, httpx_mock):
748+
httpx_mock.add_response(json=[])
749+
result = client.get_categories()
750+
assert result == []
751+
752+
def test_get_categories_non_list(self, client: PolymarketClient, httpx_mock):
753+
httpx_mock.add_response(json={"error": "bad"})
754+
result = client.get_categories()
755+
assert result == []
756+
757+
def test_get_categories_cached(self, client: PolymarketClient, httpx_mock):
758+
cats = [{"label": "Politics", "slug": "politics"}]
759+
httpx_mock.add_response(json=cats)
760+
result1 = client.get_categories()
761+
assert len(result1) == 1
762+
# Second call should use cache (no extra HTTP request)
763+
result2 = client.get_categories()
764+
assert len(result2) == 1
765+
698766

699767
# ---------------------------------------------------------------------------
700768
# Line 213: get_tick_size cache hit path

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,3 +1077,24 @@ def test_web_command_exists(self):
10771077
assert "Start the web dashboard server" in result.output
10781078
assert "--port" in result.output
10791079
assert "--host" in result.output
1080+
assert "--reload" in result.output
1081+
1082+
@patch("uvicorn.run")
1083+
def test_web_no_reload(self, mock_run):
1084+
runner = click.testing.CliRunner()
1085+
result = runner.invoke(main, ["web", "--port", "9999"])
1086+
assert result.exit_code == 0
1087+
assert "Starting dashboard" in result.output
1088+
mock_run.assert_called_once()
1089+
assert mock_run.call_args[1]["port"] == 9999
1090+
assert "reload" not in mock_run.call_args[1]
1091+
1092+
@patch("uvicorn.run")
1093+
def test_web_reload(self, mock_run):
1094+
runner = click.testing.CliRunner()
1095+
result = runner.invoke(main, ["web", "--port", "9999", "--reload"])
1096+
assert result.exit_code == 0
1097+
mock_run.assert_called_once_with(
1098+
"pm_trader.web.app:create_app", factory=True,
1099+
host="127.0.0.1", port=9999, reload=True,
1100+
)

tests/test_web/test_dashboard.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ def test_get_trades_with_filters(self, client, mock_engine):
7777
limit=10, offset=5, side="buy", market_slug="test-slug",
7878
)
7979

80+
def test_get_trades_with_data(self, client, mock_engine):
81+
mock_trade = MagicMock()
82+
mock_trade.id = 1
83+
mock_trade.market_condition_id = "0xabc"
84+
mock_trade.market_slug = "test-slug"
85+
mock_trade.market_question = "Test?"
86+
mock_trade.outcome = "yes"
87+
mock_trade.side = "buy"
88+
mock_trade.order_type = "fok"
89+
mock_trade.avg_price = 0.65
90+
mock_trade.amount_usd = 100.0
91+
mock_trade.shares = 153.85
92+
mock_trade.fee_rate_bps = 200
93+
mock_trade.fee_paid = 1.30
94+
mock_trade.timestamp = "2026-03-12T00:00:00"
95+
mock_engine.db = MagicMock()
96+
mock_engine.db.get_trades.return_value = [mock_trade]
97+
resp = client.get("/api/trades")
98+
assert resp.status_code == 200
99+
data = resp.json()["data"]
100+
assert len(data) == 1
101+
assert data[0]["market_slug"] == "test-slug"
102+
80103

81104
class TestChartPnl:
82105
def test_chart_pnl(self, client, mock_engine):

tests/test_web/test_markets.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi.testclient import TestClient
55
from pm_trader.web.app import create_app
66
from pm_trader.models import OrderBook, TradeResult
7+
from pm_trader.web.routes.markets import _serialize
78

89
@pytest.fixture
910
def client():
@@ -22,6 +23,25 @@ def test_search(self, client, mock_engine):
2223
assert resp.status_code == 200
2324
assert resp.json()["ok"] is True
2425

26+
def test_list_by_tag(self, client, mock_engine):
27+
mock_engine.api.get_markets_by_tag.return_value = []
28+
resp = client.get("/api/markets?tag=politics")
29+
assert resp.status_code == 200
30+
assert resp.json()["ok"] is True
31+
mock_engine.api.get_markets_by_tag.assert_called_once()
32+
33+
class TestGetCategories:
34+
def test_categories(self, client, mock_engine):
35+
mock_engine.api.get_categories.return_value = [
36+
{"label": "Politics", "slug": "politics"},
37+
{"label": "Sports", "slug": "sports"},
38+
]
39+
resp = client.get("/api/categories")
40+
assert resp.status_code == 200
41+
data = resp.json()["data"]
42+
assert len(data) == 2
43+
assert data[0]["label"] == "Politics"
44+
2545
class TestGetTags:
2646
def test_tags(self, client, mock_engine):
2747
mock_engine.api.get_tags.return_value = [{"label": "Politics", "slug": "politics"}]
@@ -53,3 +73,14 @@ def test_sell(self, client, mock_engine):
5373
mock_engine.sell.return_value = TradeResult(trade=mock_trade, account=mock_account)
5474
resp = client.post("/api/sell", json={"slug": "test-market", "outcome": "yes", "shares": 100.0, "order_type": "fok"})
5575
assert resp.status_code == 200
76+
77+
78+
class TestSerialize:
79+
def test_serialize_list(self):
80+
assert _serialize([1, 2, 3]) == [1, 2, 3]
81+
82+
def test_serialize_dict(self):
83+
assert _serialize({"a": 1}) == {"a": 1}
84+
85+
def test_serialize_nested(self):
86+
assert _serialize({"a": [1, {"b": 2}]}) == {"a": [1, {"b": 2}]}

0 commit comments

Comments
 (0)