trading 不再管理 ラクマ 的购物车购买、支付与订单监控。删除 auth_site / auth_session / login_runner / models.AuthSite 中的 rakuma 分支与常量、 account.yaml.example 的 rakuma 段,并清理相关测试。scraping 侧的 ラクマ 抓取 API(/api/rakuma/*)保留不动。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
4.6 KiB
Python
158 lines
4.6 KiB
Python
"""交易服务 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"]
|