From 55251c93861561c140ea864da27f14f65c8fa028 Mon Sep 17 00:00:00 2001 From: alooshxl Date: Thu, 13 Aug 2026 23:12:07 +0300 Subject: [PATCH] fix(xueqiu): validate hot-stock limits --- agent_reach/channels/xueqiu.py | 5 +++ tests/test_xueqiu_channel.py | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/agent_reach/channels/xueqiu.py b/agent_reach/channels/xueqiu.py index 1ecf45b5..4b1df350 100644 --- a/agent_reach/channels/xueqiu.py +++ b/agent_reach/channels/xueqiu.py @@ -283,6 +283,11 @@ def get_hot_stocks(self, limit: int = 10, stock_type: int = 10) -> list: Returns a list of dicts with keys: symbol, name, current, percent, rank """ + if limit < 0: + raise ValueError("limit must be non-negative") + limit = min(limit, 50) + if limit == 0: + return [] data = _get_json( f"https://stock.xueqiu.com/v5/stock/hot_stock/list.json" f"?size={limit}&type={stock_type}" diff --git a/tests/test_xueqiu_channel.py b/tests/test_xueqiu_channel.py index 4b1ace2b..5f5ac541 100644 --- a/tests/test_xueqiu_channel.py +++ b/tests/test_xueqiu_channel.py @@ -280,3 +280,68 @@ def test_get_hot_stocks_empty_when_no_items(): ch = XueqiuChannel() with patch.object(xq, "_get_json", return_value={"data": {}}): assert ch.get_hot_stocks() == [] + + +def test_get_hot_stocks_requests_the_requested_size(): + ch = XueqiuChannel() + captured = {} + + def fake_get_json(url): + captured["url"] = url + return {"data": {"items": []}} + + with patch.object(xq, "_get_json", side_effect=fake_get_json): + ch.get_hot_stocks(limit=12, stock_type=12) + + query = parse_qs(urlsplit(captured["url"]).query) + assert query["size"] == ["12"] + assert query["type"] == ["12"] + + +def test_get_hot_stocks_clamps_size_to_documented_maximum(): + ch = XueqiuChannel() + captured = {} + payload = { + "data": { + "items": [ + { + "code": f"SH{i:06d}", + "name": f"Stock {i}", + "current": i, + "percent": 0, + } + for i in range(60) + ] + } + } + + def fake_get_json(url): + captured["url"] = url + return payload + + with patch.object(xq, "_get_json", side_effect=fake_get_json): + stocks = ch.get_hot_stocks(limit=500) + + assert parse_qs(urlsplit(captured["url"]).query)["size"] == ["50"] + assert len(stocks) == 50 + + +def test_get_hot_stocks_zero_limit_skips_network(): + ch = XueqiuChannel() + with patch.object( + xq, + "_get_json", + side_effect=AssertionError("zero limit must not make a request"), + ): + assert ch.get_hot_stocks(limit=0) == [] + + +def test_get_hot_stocks_rejects_negative_limit_before_network(): + ch = XueqiuChannel() + with patch.object( + xq, + "_get_json", + side_effect=AssertionError("negative limit must not make a request"), + ): + with pytest.raises(ValueError, match="^limit must be non-negative$"): + ch.get_hot_stocks(limit=-1)