Files
MoviePilot/tests/test_subscribe_api_auth.py

129 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
验证订阅全量查询路由真实执行 API_TOKEN 认证后才读取数据。
"""
from collections.abc import Iterator
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.adapters.web.security import access
from app.api.endpoints import subscribe as subscribe_endpoint
from app.schemas.subscribe import Subscribe
from app.schemas.token import TokenPayload
@pytest.fixture
def subscribe_api(monkeypatch) -> Iterator[SimpleNamespace]:
"""
保留真实认证依赖,仅替换配置、管理员身份端口和订阅查询边界。
:param monkeypatch: 用例结束后恢复配置读取及身份端口的 pytest 夹具
:yields: 独立 HTTP 客户端及用于验证读取和身份校验的替身
"""
monkeypatch.setattr(access, "get_runtime_setting", lambda _key: "test-subscription-api-token")
identity = Mock(return_value=TokenPayload(sub=7, username="admin", super_user=True))
monkeypatch.setattr(access, "_superuser_token_payload_provider", identity)
query = SimpleNamespace(
count_public=AsyncMock(return_value=2),
list_public=AsyncMock(return_value=[
Subscribe(id=1, name="订阅一", username="alice"),
Subscribe(id=2, name="订阅二", username="bob"),
]),
)
status = SimpleNamespace(for_subscriptions=AsyncMock(return_value={}))
app = FastAPI()
app.include_router(subscribe_endpoint.router, prefix="/api/v1/subscribe")
app.dependency_overrides[subscribe_endpoint.get_subscription_query_service] = lambda: query
app.dependency_overrides[subscribe_endpoint.get_subscription_execution_status_service] = lambda: status
with TestClient(app) as client:
yield SimpleNamespace(client=client, query=query, status=status, identity=identity)
@pytest.mark.parametrize(
("params", "headers"),
[
({}, {}),
({"token": ""}, {}),
({"token": "wrong-token"}, {}),
({}, {"X-API-KEY": "wrong-token"}),
({"_": "test-subscription-api-token"}, {}),
({"token": "test-subscription-api-token"}, {"X-API-KEY": "wrong-token"}),
],
)
def test_subscribe_list_rejects_invalid_credentials_before_reading(subscribe_api, params, headers) -> None:
"""
缺失或错误凭据必须返回 401,不能查询订阅总量、数据或执行状态。
"""
response = subscribe_api.client.get("/api/v1/subscribe/list", params=params, headers=headers)
assert response.status_code == 401
assert response.headers["www-authenticate"] == "Bearer"
assert response.json() == {"detail": "token 校验不通过"}
subscribe_api.query.count_public.assert_not_awaited()
subscribe_api.query.list_public.assert_not_awaited()
subscribe_api.status.for_subscriptions.assert_not_awaited()
subscribe_api.identity.assert_not_called()
@pytest.mark.parametrize(
("params", "headers"),
[
({"token": "test-subscription-api-token"}, {}),
({}, {"X-API-KEY": "test-subscription-api-token"}),
({"token": "wrong-token"}, {"X-API-KEY": "test-subscription-api-token"}),
],
)
def test_subscribe_list_accepts_valid_credentials_and_preserves_global_query(subscribe_api, params, headers) -> None:
"""
正确凭据通过当前管理员校验后,仍按既有分页合同读取全部用户订阅。
"""
response = subscribe_api.client.get(
"/api/v1/subscribe/list",
params={**params, "page": 1, "count": 2},
headers=headers,
)
assert response.status_code == 200
assert response.headers["x-total-count"] == "2"
assert [(item["id"], item["username"]) for item in response.json()["data"]] == [(1, "alice"), (2, "bob")]
subscribe_api.query.count_public.assert_awaited_once_with()
subscribe_api.query.list_public.assert_awaited_once_with(page=1, count=2)
subscribe_api.status.for_subscriptions.assert_awaited_once_with((1, 2))
subscribe_api.identity.assert_called_once_with()
def test_subscribe_list_rejects_invalid_admin_identity_before_reading(subscribe_api) -> None:
"""
正确 API_TOKEN 也不能绕过所绑定管理员的当前身份校验。
"""
subscribe_api.identity.side_effect = PermissionError("用户不存在或已禁用")
response = subscribe_api.client.get(
"/api/v1/subscribe/list", params={"token": "test-subscription-api-token"}
)
assert response.status_code == 401
assert response.headers["www-authenticate"] == "Bearer"
subscribe_api.identity.assert_called_once_with()
subscribe_api.query.count_public.assert_not_awaited()
subscribe_api.query.list_public.assert_not_awaited()
subscribe_api.status.for_subscriptions.assert_not_awaited()
def test_subscribe_list_openapi_declares_authentication(subscribe_api) -> None:
"""
OpenAPI 应声明 API 凭据,不能把认证占位参数暴露为普通查询参数。
"""
response = subscribe_api.client.get("/openapi.json")
assert response.status_code == 200
operation = response.json()["paths"]["/api/v1/subscribe/list"]["get"]
assert {"api_token_query": []} in operation["security"]
assert {"api_key_header": []} in operation["security"]
assert "_" not in {parameter["name"] for parameter in operation.get("parameters", [])}