"""交易服务 API 测试:健康检查、登录态查询与重载、鉴权 登录态会话被替换为桩,不触达真实站点、不需要真实账号。 AuthSession 自身的行为(cookie 加载、探测判据)在 tests/test_auth_session.py。 """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from app.shared.config import get_settings from app.shared.errors import NotLoggedInError from app.trading.main import create_app from app.trading.services.auth_session import AuthStatus TOKEN = get_settings().bearer_token AUTH = {"Authorization": f"Bearer {TOKEN}"} class StubAuthSession: """记录调用并返回固定登录态的桩""" def __init__(self) -> None: self.checked: list[str] = [] self.reloaded: list[str] = [] self.logged_in = True @property def sites(self) -> tuple[str, ...]: return ("rakuten",) def _status(self, site: str) -> AuthStatus: return AuthStatus( site=site, state_file_exists=True, logged_in=self.logged_in, checked_at=None, detail="stub", ) async def check(self, site: str) -> AuthStatus: self.checked.append(site) return self._status(site) def status(self, site: str) -> AuthStatus: return self._status(site) def status_all(self) -> dict[str, dict]: return {site: self._status(site).to_dict() for site in self.sites} def reload(self, site: str) -> int: self.reloaded.append(site) return 3 async def require_logged_in(self, site: str) -> None: if not self.logged_in: raise NotLoggedInError(site=site, detail="stub") async def close(self) -> None: """lifespan 收尾会调用;桩没有真实客户端要关""" @pytest.fixture def client_and_stub(): app = create_app() with TestClient(app) as client: stub = StubAuthSession() app.state.container.auth_session = stub yield client, stub @pytest.fixture def client(client_and_stub): return client_and_stub[0] @pytest.fixture def stub(client_and_stub): return client_and_stub[1] # ---- 健康检查 ---- def test_health_needs_no_token(client): response = client.get("/health") assert response.status_code == 200 body = response.json() assert body["data"]["status"] == "ok" assert set(body["data"]["auth"]) == {"rakuten"} def test_health_does_not_probe_the_site(client, stub): """健康检查只读缓存:它会被高频轮询,不能每次都去打站点""" client.get("/health") assert stub.checked == [] # ---- 鉴权 ---- @pytest.mark.parametrize("path", ["/api/auth/status", "/api/auth/reload"]) def test_auth_endpoints_reject_missing_token(client, path): response = client.post(path, json={}) assert response.status_code == 401 assert response.json()["code"] == 1001 def test_auth_endpoints_reject_wrong_token(client): response = client.post( "/api/auth/status", json={}, headers={"Authorization": "Bearer wrong-token"} ) assert response.status_code == 401 # ---- 登录态查询 ---- def test_status_probes_site_by_default(client, stub): response = client.post("/api/auth/status", json={}, headers=AUTH) assert response.status_code == 200 body = response.json() assert [item["site"] for item in body["data"]["sites"]] == ["rakuten"] assert stub.checked == ["rakuten"] def test_status_can_skip_the_probe(client, stub): """refresh=false 时读缓存,不打站点""" response = client.post("/api/auth/status", json={"refresh": False}, headers=AUTH) assert response.status_code == 200 assert stub.checked == [] def test_status_accepts_a_single_site(client, stub): response = client.post("/api/auth/status", json={"site": "rakuten"}, headers=AUTH) assert response.status_code == 200 assert stub.checked == ["rakuten"] def test_status_rejects_unknown_site(client): """站点名是枚举,未知值应在校验层就被挡下""" response = client.post("/api/auth/status", json={"site": "mercari"}, headers=AUTH) assert response.status_code == 422 assert response.json()["code"] == 1002 # ---- 登录态重载 ---- def test_reload_reloads_then_probes(client, stub): response = client.post("/api/auth/reload", json={"site": "rakuten"}, headers=AUTH) assert response.status_code == 200 body = response.json() assert body["data"]["reloaded"] == {"rakuten": 3} # 重载后必须立刻探测一次,否则调用方拿不到「这次登录到底成没成」 assert stub.reloaded == ["rakuten"] assert stub.checked == ["rakuten"]