Files
shaguabijia-app-server/tests/test_admin.py
T
OuYingJun1024 1a7a624b87 feat(admin): 列表页码分页——CursorPage 加 total,offset 分页支持跳页
用户/提现/上报/审计日志四个主列表页从「加载更多」改为页码分页:
- CursorPage 加 total 字段(可选,不影响其它接口)
- queries 新增 offset_paginate(items, next_cursor, total);count 与分页同源,
  筛选条件一处构建避免漂移;list_users/withdraw/price_reports 接入返回 total
- price_reports / audit_logs 从 id 游标改 offset 分页(cursor 即 offset),支持跳页
- 四个 router 透出 total;withdraw 详情内部调用同步解包
- 反馈页本轮排除(其 router/model 正在 feat/feedback-iteration 迭代,避免纠缠)

测试: test_audit_log_pagination 改为校验 offset+total;admin 套件 47 passed。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:36:57 +08:00

157 lines
5.8 KiB
Python
Raw 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.
"""Admin 后台 M1 测试:登录闭环 + 与 App 用户鉴权的彻底隔离。
admin app 是独立的 FastAPI(app.admin.main:admin_app),用独立 TestClient。
admin 表由 conftest 的 Base.metadata.create_all 一起建好(models/__init__ 已登记)。
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app.admin.main import admin_app
from app.admin.repositories import admin_user as admin_repo
from app.core.security import create_token, hash_password
from app.db.session import SessionLocal
@pytest.fixture()
def admin_client() -> TestClient:
return TestClient(admin_app)
def _ensure_admin(
username: str = "test_admin",
password: str = "admin-pass-123",
role: str = "super_admin",
) -> tuple[str, str]:
"""create-or-reset 一个 admin(测试 DB 跨用例共享,需幂等)。"""
db = SessionLocal()
try:
a = admin_repo.get_by_username(db, username)
if a is None:
admin_repo.create_admin(db, username=username, password=password, role=role)
else:
a.password_hash = hash_password(password)
a.role = role
a.status = "active"
db.commit()
finally:
db.close()
return username, password
def test_admin_login_and_me(admin_client: TestClient) -> None:
username, password = _ensure_admin()
r = admin_client.post(
"/admin/api/auth/login", json={"username": username, "password": password}
)
assert r.status_code == 200, r.text
data = r.json()
assert "access_token" in data
assert data["admin"]["username"] == username
assert data["admin"]["role"] == "super_admin"
token = data["access_token"]
r = admin_client.get("/admin/api/auth/me", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200, r.text
assert r.json()["username"] == username
def test_admin_login_wrong_password(admin_client: TestClient) -> None:
username, _ = _ensure_admin()
r = admin_client.post(
"/admin/api/auth/login", json={"username": username, "password": "definitely-wrong"}
)
assert r.status_code == 401
def test_admin_me_requires_token(admin_client: TestClient) -> None:
assert admin_client.get("/admin/api/auth/me").status_code == 401
def test_disabled_admin_cannot_login(admin_client: TestClient) -> None:
username, password = _ensure_admin(username="disabled_admin")
db = SessionLocal()
try:
a = admin_repo.get_by_username(db, username)
a.status = "disabled"
db.commit()
finally:
db.close()
r = admin_client.post(
"/admin/api/auth/login", json={"username": username, "password": password}
)
assert r.status_code == 403
# ============================ 关键:鉴权隔离 ============================
def test_app_user_token_cannot_access_admin(admin_client: TestClient) -> None:
"""App 用户的 access_token(用 JWT_SECRET_KEY 签)不能访问 admin。
admin 用独立 ADMIN_JWT_SECRET 验签 → App token 直接验签失败 → 401。
这是后台防越权的第一道线。
"""
user_token, _ = create_token(user_id=1, token_type="access")
r = admin_client.get(
"/admin/api/auth/me", headers={"Authorization": f"Bearer {user_token}"}
)
assert r.status_code == 401
def test_admin_token_cannot_access_app_api(client: TestClient, admin_client: TestClient) -> None:
"""反向:admin token 也不能访问 App 用户接口(App 用 JWT_SECRET_KEY 验签,admin token 失败)。"""
username, password = _ensure_admin()
r = admin_client.post(
"/admin/api/auth/login", json={"username": username, "password": password}
)
admin_token = r.json()["access_token"]
r = client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {admin_token}"})
assert r.status_code == 401
# ============================ 回归:review 修掉的两个 bug ============================
def test_long_password_does_not_crash(admin_client: TestClient) -> None:
""">72 UTF-8 字节的密码(如多个中文)不能让建账号/登录崩(bcrypt 72 字节限制)。"""
username = "longpw_admin"
long_pw = "超长密码测试" * 8 # 6 中文 ×8 = 48 字 ≈ 144 字节 UTF-8,远超 72
_ensure_admin(username=username, password=long_pw) # 建账号不应抛 ValueError
r = admin_client.post(
"/admin/api/auth/login", json={"username": username, "password": long_pw}
)
assert r.status_code == 200, r.text
def test_audit_log_pagination_no_gap() -> None:
"""审计分页跨页不丢/不重(offset 分页:cursor 即 offset,翻完覆盖全部)。"""
from app.admin.repositories import admin_user as admin_repo
from app.admin.repositories import audit_log as audit_repo
_ensure_admin() # 确保有 test_admin 供 FK 引用
db = SessionLocal()
try:
admin = admin_repo.get_by_username(db, "test_admin")
action = "test.pagination.probe"
created_ids = []
for i in range(5):
log = audit_repo.add_audit_log(
db, admin_id=admin.id, admin_username=admin.username,
action=action, target_type="probe", target_id=str(i),
)
created_ids.append(log.id)
# limit=2 翻 5 条,收集所有 id,应正好覆盖创建的 5 条(无丢无重);total 恒为符合条件总数
seen: list[int] = []
cursor = None
for _ in range(10): # 上限防死循环
rows, cursor, total = audit_repo.list_audit_logs(db, action=action, limit=2, cursor=cursor)
seen.extend(r.id for r in rows)
assert total == len(created_ids), f"total 应为 {len(created_ids)},得 {total}"
if cursor is None:
break
assert sorted(seen) == sorted(created_ids), f"分页丢/重: want={created_ids} got={seen}"
finally:
db.close()