From 4d4327e25acc631a84b5205609297451e1dbe598 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 12 Sep 2026 21:11:14 +0800 Subject: [PATCH 1/2] fix(site): report unauthenticated userdata refresh failures --- app/api/endpoints/site.py | 9 +++++++- app/chain/site.py | 4 ++-- app/modules/indexer/__init__.py | 2 ++ tests/test_indexer_userdata_fallback.py | 1 + tests/test_site_connectivity.py | 27 +++++++++++++++++++++++ tests/test_site_cookie_endpoint.py | 29 +++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index 5ae47b695..2118d4400 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -488,7 +488,14 @@ def refresh_userdata( return _SchemaResponse( success=False, message="站点不支持索引或未通过用户认证!" ) - user_data = SiteChain().refresh_userdata(site=indexer) or {} + user_data = SiteChain().refresh_userdata(site=indexer) + if not user_data or not user_data.userid: + message = ( + user_data.err_msg + if user_data and user_data.err_msg + else "未获取到站点用户数据,请检查 Cookie 是否有效!" + ) + return _SchemaResponse(success=False, message=message) return _SchemaResponse(success=True, data=user_data) diff --git a/app/chain/site.py b/app/chain/site.py index 62ff7c090..1fe1d1368 100644 --- a/app/chain/site.py +++ b/app/chain/site.py @@ -212,7 +212,7 @@ class SiteChain(InteractionChainMixin, ChainBase): :return: 用户数据 """ userdata: SiteUserData = self.run_module("refresh_userdata", site=site) - if userdata and site: + if userdata and userdata.userid and site: domain = site_rules.extract_domain( str(site.get("domain") or site.get("url") or "") ) @@ -302,7 +302,7 @@ class SiteChain(InteractionChainMixin, ChainBase): }, ) userdata = self.refresh_userdata(site) - if userdata: + if userdata and userdata.userid: any_site_updated = True result[site.get("name")] = userdata if progress_callback: diff --git a/app/modules/indexer/__init__.py b/app/modules/indexer/__init__.py index 210fdee18..cd1f800e0 100644 --- a/app/modules/indexer/__init__.py +++ b/app/modules/indexer/__init__.py @@ -799,6 +799,8 @@ class IndexerModule(_ModuleBase): site_obj = alt_obj logger.info(f"站点 {site.get('name')} 改用 {site_schema.schema.value} 模型解析成功") break + if not site_obj.userid and not site.get("public"): + site_obj.err_msg = site_obj.err_msg or "未获取到站点用户信息,请检查 Cookie 是否有效" return SiteUserData( domain=site_rules.extract_domain(site.get("url")), userid=site_obj.userid, diff --git a/tests/test_indexer_userdata_fallback.py b/tests/test_indexer_userdata_fallback.py index 98b654cb3..01d9fc6b4 100644 --- a/tests/test_indexer_userdata_fallback.py +++ b/tests/test_indexer_userdata_fallback.py @@ -59,6 +59,7 @@ def test_refresh_userdata_fallback_only_uses_common_schemas(monkeypatch): }) assert result.userid is None + assert result.err_msg == "未获取到站点用户信息,请检查 Cookie 是否有效" assert calls == [ SiteSchema.NexusPhp.value, SiteSchema.Gazelle.value, diff --git a/tests/test_site_connectivity.py b/tests/test_site_connectivity.py index 7d508d347..61273fe7e 100644 --- a/tests/test_site_connectivity.py +++ b/tests/test_site_connectivity.py @@ -1,9 +1,12 @@ """站点连通性测试链路的数据库回归测试。""" +from unittest.mock import Mock + from app.application.site.contract import SiteSnapshot from app.chain.site import SiteChain from app.db.models.site import Site from app.db.models.sitestatistic import SiteStatistic +from app.schemas.site import SiteUserData def test_site_connectivity_records_result_without_injected_session(db, monkeypatch): @@ -59,3 +62,27 @@ def test_resource_login_path_uses_generic_connectivity_test(db, monkeypatch): assert (status, message) == (True, "连接成功") assert observed[0].url == "https://resource-path.test/index.php" + + +def test_failed_userdata_is_not_persisted_as_a_successful_refresh(monkeypatch): + """解析器未取得用户身份时不得写入空数据或发送刷新成功事件。""" + chain = object.__new__(SiteChain) + chain.site_repository = Mock() + chain.eventmanager = Mock() + failed_data = SiteUserData( + domain="www.musopia.vip", + err_msg="未检测到已登陆,请检查cookies是否过期", + ) + monkeypatch.setattr( + SiteChain, + "run_module", + lambda _self, _method, **_kwargs: failed_data, + ) + + result = chain.refresh_userdata( + site={"id": 1, "domain": "www.musopia.vip", "name": "音乐乌托邦"} + ) + + assert result is failed_data + chain.site_repository.update_userdata.assert_not_called() + chain.eventmanager.send_event.assert_not_called() diff --git a/tests/test_site_cookie_endpoint.py b/tests/test_site_cookie_endpoint.py index efaa7f8f7..375abe5a1 100644 --- a/tests/test_site_cookie_endpoint.py +++ b/tests/test_site_cookie_endpoint.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch from app import schemas from app.api.endpoints import site as site_endpoint +from app.schemas.site import SiteUserData def test_update_cookie_by_body_uses_request_body(): @@ -85,3 +86,31 @@ def test_set_cookie_by_body_persists_only_browser_cookie_fields(): cookie="sid=browser", ua="Browser UA", ) + + +def test_refresh_userdata_reports_parser_failure_instead_of_empty_success(): + """用户解析未拿到身份时应返回业务失败,不能伪造空数据成功。""" + fake_site = SimpleNamespace(id=1, domain="www.musopia.vip") + fake_chain = Mock() + fake_chain.refresh_userdata.return_value = SiteUserData( + domain="www.musopia.vip", + err_msg="未检测到已登陆,请检查cookies是否过期", + ) + + with patch.object(site_endpoint, "SiteChain", return_value=fake_chain), patch.object( + site_endpoint, + "SitesHelper", + ) as sites_helper: + sites_helper.return_value.get_indexer.return_value = { + "name": "音乐乌托邦", + "domain": "www.musopia.vip", + } + response = site_endpoint.refresh_userdata( + site_id=1, + query=SimpleNamespace(get_sync=lambda _site_id: fake_site), + _=Mock(), + ) + + assert response.success is False + assert response.message == "未检测到已登陆,请检查cookies是否过期" + assert response.data is None From f6c647ce5b40a5a9e73e16eb7455dc09d9450831 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 12 Sep 2026 21:29:55 +0800 Subject: [PATCH 2/2] chore(architecture): keep subscription query within budget --- app/chain/subscribe/query.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/chain/subscribe/query.py b/app/chain/subscribe/query.py index 41b5397ea..ec4c38a7a 100644 --- a/app/chain/subscribe/query.py +++ b/app/chain/subscribe/query.py @@ -65,10 +65,8 @@ class SubscribeQueryOwner(_SubscribeOwnerBase): def get_subscribed_sites(self, mtype: Optional[str] = None) -> Optional[List[int]]: """ 获取指定媒体类型订阅涉及的站点清单(节约资源) - - 未指定媒体类型时保持原有全量订阅行为;指定类型却没有可搜索订阅时 - 返回 None,避免调用方把空站点误解为“搜索所有站点”。 - + 未指定媒体类型时保持原有全量订阅行为;指定类型却没有可搜索订阅时返回 None, + 避免调用方把空站点误解为“搜索所有站点”。 :param mtype: 可选媒体类型 :return: 返回[]代表所有站点命中,返回None代表没有订阅 """