diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index e7b621e4c..0cb4bc361 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -2,7 +2,7 @@ 提供站点配置、认证、数据查询和状态管理接口。 """ -from typing import Annotated, Any, List, Literal, Optional, cast +from typing import Annotated, Any, List, Literal, Optional from fastapi import Depends, HTTPException, Query, Response @@ -691,11 +691,13 @@ async def site_category( category_value = indexer.get("category") if not isinstance(category_value, dict): return [] - category = cast(dict[str, list[dict[str, Any]]], category_value) - result = [] - for cats in category.values(): + # field、delimiter、param 等是搜索请求参数,只有列表值才是分类项 + result: list[dict[str, Any]] = [] + for cats in category_value.values(): + if not isinstance(cats, list): + continue for cat in cats: - if cat not in result: + if isinstance(cat, dict) and cat not in result: result.append(cat) return result diff --git a/tests/test_api_optional_inputs.py b/tests/test_api_optional_inputs.py index 765e04388..bbec5e84e 100644 --- a/tests/test_api_optional_inputs.py +++ b/tests/test_api_optional_inputs.py @@ -8,12 +8,14 @@ from unittest.mock import AsyncMock, Mock import pytest from fastapi import HTTPException +from pydantic import TypeAdapter from app.api.endpoints import bangumi, download, history, mediaserver, message, search, site, tmdb, transfer from app.domain.context import MediaInfo from app.schemas.context import MediaInfo as MediaInfoInput from app.schemas.context import SubtitleInfo from app.schemas.file import FileItem +from app.schemas.site import SiteCategory from app.schemas.transfer import ManualTransferItem from app.schemas.types import MediaType @@ -124,6 +126,28 @@ def test_site_category_only_projects_category_mappings(category, monkeypatch): assert result == ([{"id": 1}, {"id": 2}] if isinstance(category, dict) else []) +def test_site_category_skips_search_parameter_settings(monkeypatch): + """ + 分类配置中的 field、delimiter、param 等搜索参数不能被当成分类项逐字符返回。 + """ + category = { + "field": "cat[]", + "delimiter": "&", + "param": "cat", + "movie": [{"id": 401, "cat": "Movies", "desc": "电影"}, "invalid"], + "tv": [{"id": 402, "cat": "TV", "desc": "剧集"}], + } + query = SimpleNamespace(get=AsyncMock(return_value=SimpleNamespace(domain="https://example.test"))) + helper = SimpleNamespace(async_get_indexer=AsyncMock(return_value={"category": category})) + monkeypatch.setattr(site, "SitesHelper", Mock(return_value=helper)) + result = asyncio.run(site.site_category(1, query=query, _=None)) + assert result == [{"id": 401, "cat": "Movies", "desc": "电影"}, {"id": 402, "cat": "TV", "desc": "剧集"}] + assert TypeAdapter(list[SiteCategory]).validate_python(result) == [ + SiteCategory(id=401, cat="Movies", desc="电影"), + SiteCategory(id=402, cat="TV", desc="剧集"), + ] + + @pytest.mark.parametrize("batch", [False, True]) @pytest.mark.parametrize("moved", [False, True]) def test_manual_transfer_history_without_file_snapshot_returns_business_error(batch, moved):