Skip to content

Commit bd2be06

Browse files
fix(api): 处理 PR 审查评论
- 添加 _client None 检查,防止 close() 后调用 - from_dict 增加列表元素类型校验 - 嵌套对象字段增加类型校验 - Cookie 选择改用 Playwright URL 作用域 - 测试文件添加 sys.path 注入 - pyproject.toml 添加 respx 到 test 依赖 - points_text 添加 None 检查
1 parent 156d234 commit bd2be06

5 files changed

Lines changed: 56 additions & 38 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ test = [
4848
"pytest-xdist>=3.5.0",
4949
"hypothesis>=6.125.0",
5050
"faker>=35.0.0",
51+
"httpx>=0.28.0",
52+
"respx>=0.21.0",
5153
]
5254
viz = [
5355
"streamlit>=1.41.0",

src/account/points_detector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def get_current_points(self, page: Page, skip_navigation: bool = False) ->
125125
points_text = await element.text_content()
126126
logger.debug(f"找到积分文本: {points_text}")
127127

128-
if points_text.strip():
128+
if points_text and points_text.strip():
129129
points = self._parse_points(points_text.strip())
130130

131131
if points is not None and points >= 100:

src/api/dashboard_client.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,23 +96,14 @@ async def _get_cookies_header(self) -> str:
9696
"""
9797
从 Page context 获取 cookies 字符串
9898
99-
只发送与 API 域名相关的 Cookie,避免跨域泄露。
99+
使用 Playwright 的 URL 作用域 cookie 选择,让 Playwright 按浏览器规则
100+
返回对该 URL 生效的 cookies(包括父域 cookies)。
100101
101102
Returns:
102103
cookies 字符串
103104
"""
104-
from urllib.parse import urlparse
105-
106-
all_cookies = await self._page.context.cookies()
107-
api_domain = urlparse(self._api_url).netloc
108-
109-
allowed_domains = {
110-
api_domain,
111-
f".{api_domain}",
112-
}
113-
114-
filtered_cookies = [c for c in all_cookies if c["domain"] in allowed_domains]
115-
return "; ".join(f"{c['name']}={c['value']}" for c in filtered_cookies)
105+
cookies = await self._page.context.cookies([self._api_url])
106+
return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
116107

117108
async def _call_api(self) -> DashboardData:
118109
"""
@@ -133,6 +124,8 @@ async def _call_api(self) -> DashboardData:
133124

134125
for attempt in range(self._max_retries + 1):
135126
try:
127+
if self._client is None or self._client.is_closed:
128+
raise DashboardError("HTTP client has been closed")
136129
response = await self._client.get(self._api_url, headers=headers)
137130
response.raise_for_status()
138131
data = response.json()

src/api/models.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ def from_dict(cls, data: dict[str, Any]) -> "SearchCounters":
6464
if not isinstance(mobile_raw, list):
6565
mobile_raw = []
6666

67-
pc_search = [SearchCounter.from_dict(item) for item in pc_raw]
68-
mobile_search = [SearchCounter.from_dict(item) for item in mobile_raw]
67+
pc_search = [SearchCounter.from_dict(item) for item in pc_raw if isinstance(item, dict)]
68+
mobile_search = [
69+
SearchCounter.from_dict(item) for item in mobile_raw if isinstance(item, dict)
70+
]
6971
return cls(pc_search=pc_search, mobile_search=mobile_search)
7072

7173

@@ -171,8 +173,16 @@ class UserStatus:
171173
def from_dict(cls, data: dict[str, Any]) -> "UserStatus":
172174
"""从字典创建实例"""
173175
data = _transform_dict(data)
174-
level_info = LevelInfo.from_dict(data.get("level_info", {}))
175-
counters = SearchCounters.from_dict(data.get("counters", {}))
176+
level_info_raw = data.get("level_info")
177+
level_info = (
178+
LevelInfo.from_dict(level_info_raw) if isinstance(level_info_raw, dict) else LevelInfo()
179+
)
180+
counters_raw = data.get("counters")
181+
counters = (
182+
SearchCounters.from_dict(counters_raw)
183+
if isinstance(counters_raw, dict)
184+
else SearchCounters()
185+
)
176186
return cls(
177187
available_points=data.get("available_points", 0),
178188
level_info=level_info,
@@ -219,22 +229,29 @@ def from_dict(cls, data: dict[str, Any]) -> "DashboardData":
219229
more_promotions_data = data.get("more_promotions") or []
220230
if not isinstance(more_promotions_data, list):
221231
more_promotions_data = []
222-
more_promotions = [Promotion.from_dict(item) for item in more_promotions_data]
232+
more_promotions = [
233+
Promotion.from_dict(item) for item in more_promotions_data if isinstance(item, dict)
234+
]
223235

224236
punch_cards_data = data.get("punch_cards") or []
225237
if not isinstance(punch_cards_data, list):
226238
punch_cards_data = []
227-
punch_cards = [PunchCard.from_dict(item) for item in punch_cards_data]
239+
punch_cards = [
240+
PunchCard.from_dict(item) for item in punch_cards_data if isinstance(item, dict)
241+
]
228242

229243
streak_promotion = None
230-
if data.get("streak_promotion"):
231-
streak_promotion = StreakPromotion.from_dict(data["streak_promotion"])
244+
streak_promotion_raw = data.get("streak_promotion")
245+
if isinstance(streak_promotion_raw, dict):
246+
streak_promotion = StreakPromotion.from_dict(streak_promotion_raw)
232247

233248
streak_bonus_data = data.get("streak_bonus_promotions") or []
234249
if not isinstance(streak_bonus_data, list):
235250
streak_bonus_data = []
236251
streak_bonus_promotions = [
237-
StreakBonusPromotion.from_dict(item) for item in streak_bonus_data
252+
StreakBonusPromotion.from_dict(item)
253+
for item in streak_bonus_data
254+
if isinstance(item, dict)
238255
]
239256

240257
return cls(

tests/unit/test_dashboard_client.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Dashboard Client 单元测试"""
22

3+
import sys
4+
from pathlib import Path
35
from unittest.mock import AsyncMock, Mock
46

57
import httpx
68
import pytest
79
import respx
810

11+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
12+
913
from api.dashboard_client import DashboardClient, DashboardError
1014
from api.models import DashboardData, SearchCounters
1115

@@ -15,12 +19,14 @@ def mock_page():
1519
"""Mock Playwright Page 对象"""
1620
page = Mock()
1721
page.context = Mock()
18-
page.context.cookies = AsyncMock(
19-
return_value=[
22+
23+
async def mock_cookies(urls=None):
24+
return [
2025
{"name": "cookie1", "value": "value1", "domain": "rewards.bing.com"},
2126
{"name": "cookie2", "value": "value2", "domain": ".rewards.bing.com"},
2227
]
23-
)
28+
29+
page.context.cookies = mock_cookies
2430
page.content = AsyncMock(
2531
return_value="""
2632
<html>
@@ -58,9 +64,11 @@ def mock_page_no_dashboard():
5864
"""没有 dashboard 变量的 Mock Page"""
5965
page = Mock()
6066
page.context = Mock()
61-
page.context.cookies = AsyncMock(
62-
return_value=[{"name": "cookie1", "value": "value1", "domain": "rewards.bing.com"}]
63-
)
67+
68+
async def mock_cookies(urls=None):
69+
return [{"name": "cookie1", "value": "value1", "domain": "rewards.bing.com"}]
70+
71+
page.context.cookies = mock_cookies
6472
page.content = AsyncMock(return_value="<html><body>no dashboard</body></html>")
6573
return page
6674

@@ -525,22 +533,20 @@ async def test_search_counters_handles_scalar_values():
525533

526534

527535
async def test_cookie_filtering_by_domain(mock_page):
528-
"""测试 Cookie 按域名严格过滤"""
529-
mock_page.context.cookies = AsyncMock(
530-
return_value=[
536+
"""测试 Cookie 使用 Playwright URL 作用域选择"""
537+
538+
async def mock_cookies(urls=None):
539+
return [
531540
{"name": "bing_cookie", "value": "bing_value", "domain": "bing.com"},
532541
{"name": "rewards_cookie", "value": "rewards_value", "domain": "rewards.bing.com"},
533542
{"name": "rewards_sub_cookie", "value": "sub_value", "domain": ".rewards.bing.com"},
534-
{"name": "login_cookie", "value": "login_value", "domain": "login.live.com"},
535-
{"name": "microsoft_cookie", "value": "ms_value", "domain": "microsoft.com"},
536543
]
537-
)
544+
545+
mock_page.context.cookies = mock_cookies
538546

539547
client = DashboardClient(mock_page)
540548
cookies = await client._get_cookies_header()
541549

542-
assert "bing_cookie=bing_value" not in cookies
550+
assert "bing_cookie=bing_value" in cookies
543551
assert "rewards_cookie=rewards_value" in cookies
544552
assert "rewards_sub_cookie=sub_value" in cookies
545-
assert "login_cookie=login_value" not in cookies
546-
assert "microsoft_cookie=ms_value" not in cookies

0 commit comments

Comments
 (0)