Files
shaguabijia-app-server/tests/test_admin_roles.py
T
zzhyyyyy bce8cd9923 运营后台后端:RBAC 权限管理 + 系统配置下发修复 + 比价记录店/商品搜索
一、RBAC 权限管理(角色 → 可见页面)
- 新增 admin_role 表 + 权限目录 permissions.py:角色持有一组页面 key,登录后左侧只展示这些页
- 内建角色 管理员(super_admin 全权锁定)/运营/财务/技术,页集对齐原型;key 承重(require_role 用),另加中文 label 展示
- 角色 CRUD 端点(仅 super_admin)+ 目录端点;登录/me 下发当前角色有效可见页
- admins 加删除、角色存在性校验;可复看已确定登录密码:UI 建的账号留存明文 plain_password,脚本建的超管不留存

二、系统配置下发修复
- 首页数据「保存即生效」:显式保存(apply_now)对 real/manual 直接落配置目标值、绕过只增不减护栏(修「改了 app 端不变」),护栏仍管自动 tick
- 首页轮播新增数据源三选一:mixed(真实优先+种子)/real(只真实)/seed(只种子),get_feed 分支 + /marquee-seeds/mode 端点
- 福利页 Tab 隐藏 任务/里程碑,及看广告的单次金币/每轮次数/信息流开关:CONFIG_DEFS 加 hidden + list_config 过滤,业务读取默认值不受影响

三、比价记录店/商品搜索
- comparison_record 加 product_names 派生列(从下单 items 拼商品名),迁移建列并回填历史行
- admin 比价记录列表店/商品分列可搜:走 product_names 普通列 LIKE,规避 SQLite JSON 中文 ensure_ascii 转义搜不到的坑

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:52:42 +08:00

153 lines
5.9 KiB
Python

"""Admin RBAC 角色/权限测试:可见页下发、角色 CRUD 仅 super_admin、内建/在用保护。"""
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 hash_password
from app.db.session import SessionLocal
@pytest.fixture()
def admin_client() -> TestClient:
return TestClient(admin_app)
def _token(username: str, role: str) -> str:
db = SessionLocal()
try:
a = admin_repo.get_by_username(db, username)
if a is None:
admin_repo.create_admin(db, username=username, password="pass1234", role=role)
else:
a.password_hash = hash_password("pass1234")
a.role = role
a.status = "active"
db.commit()
finally:
db.close()
c = TestClient(admin_app)
return c.post(
"/admin/api/auth/login", json={"username": username, "password": "pass1234"}
).json()["access_token"]
@pytest.fixture()
def super_token() -> str:
return _token("r_super", "super_admin")
@pytest.fixture()
def operator_token() -> str:
return _token("r_operator", "operator")
def _auth(t: str) -> dict:
return {"Authorization": f"Bearer {t}"}
def test_super_pages_all_operator_limited(admin_client, super_token, operator_token) -> None:
su = admin_client.get("/admin/api/auth/me", headers=_auth(super_token)).json()
assert "admins" in su["pages"] and "dashboard" in su["pages"] # 超管全页
op = admin_client.get("/admin/api/auth/me", headers=_auth(operator_token)).json()
assert "dashboard" in op["pages"] and "admins" not in op["pages"] # 运营看不到管理员页
def test_roles_endpoints_super_only(admin_client, super_token, operator_token) -> None:
assert admin_client.get("/admin/api/roles", headers=_auth(super_token)).status_code == 200
assert admin_client.get("/admin/api/roles", headers=_auth(operator_token)).status_code == 403
def test_role_crud(admin_client, super_token) -> None:
cat = admin_client.get("/admin/api/roles/catalog", headers=_auth(super_token)).json()
assert any(g["group"] == "看板" for g in cat)
r = admin_client.post(
"/admin/api/roles", json={"name": "审核", "pages": ["dashboard", "feedbacks", "xx-bad"]},
headers=_auth(super_token),
)
assert r.status_code == 200, r.text
rid = r.json()["id"]
assert set(r.json()["pages"]) == {"dashboard", "feedbacks"} # 非法 key 被过滤
# 重名 409
assert admin_client.post(
"/admin/api/roles", json={"name": "审核", "pages": []}, headers=_auth(super_token)
).status_code == 409
# 改可见页
u = admin_client.patch(
f"/admin/api/roles/{rid}", json={"pages": ["dashboard"]}, headers=_auth(super_token)
)
assert u.status_code == 200 and u.json()["pages"] == ["dashboard"]
# 删
assert admin_client.delete(f"/admin/api/roles/{rid}", headers=_auth(super_token)).status_code == 200
def test_builtin_super_admin_protected(admin_client, super_token) -> None:
roles = admin_client.get("/admin/api/roles", headers=_auth(super_token)).json()
sa = next(r for r in roles if r["name"] == "super_admin")
assert sa["is_builtin"] and len(sa["pages"]) >= 10 # 全部页
assert admin_client.patch(
f"/admin/api/roles/{sa['id']}", json={"pages": []}, headers=_auth(super_token)
).status_code == 400
assert admin_client.delete(
f"/admin/api/roles/{sa['id']}", headers=_auth(super_token)
).status_code == 400
def test_delete_role_in_use_blocked(admin_client, super_token) -> None:
admin_client.post(
"/admin/api/admins",
json={"username": "role_holder", "password": "pass1234", "role": "operator"},
headers=_auth(super_token),
)
roles = admin_client.get("/admin/api/roles", headers=_auth(super_token)).json()
op = next(r for r in roles if r["name"] == "operator")
assert op["in_use"] >= 1
assert admin_client.delete(
f"/admin/api/roles/{op['id']}", headers=_auth(super_token)
).status_code == 400
def test_builtin_roles_labels_and_pages(admin_client, super_token) -> None:
roles = {r["name"]: r for r in admin_client.get("/admin/api/roles", headers=_auth(super_token)).json()}
# 中文展示名(key 不变)
assert roles["super_admin"]["label"] == "管理员"
assert roles["operator"]["label"] == "运营"
assert roles["finance"]["label"] == "财务"
assert roles["tech"]["label"] == "技术"
# 页集对齐 Prototypes/dashboard/permissions.md 的 ROLES
assert set(roles["finance"]["pages"]) == {"dashboard", "ad-revenue-report", "cps", "withdraws"}
assert set(roles["tech"]["pages"]) == {
"dashboard", "device-liveness", "config", "ad-revenue", "event-logs", "audit-logs",
}
def test_ui_created_admin_shows_password_script_admin_hidden(admin_client, super_token) -> None:
# UI 建号:账号列表回显明文登录密码(供权限管理页编辑复看)
admin_client.post(
"/admin/api/admins",
json={"username": "pw_show_user", "password": "pass1234", "role": "operator"},
headers=_auth(super_token),
)
admins = {a["username"]: a for a in admin_client.get("/admin/api/admins", headers=_auth(super_token)).json()}
assert admins["pw_show_user"]["password"] == "pass1234"
# 经 repo/脚本直建的账号(r_super,无留存明文)→ password 为 None,前端不显示
assert admins["r_super"]["password"] is None
# 登录 / me 不下发明文(保持 None)
me = admin_client.get("/admin/api/auth/me", headers=_auth(super_token)).json()
assert me.get("password") is None
def test_create_admin_rejects_unknown_role(admin_client, super_token) -> None:
r = admin_client.post(
"/admin/api/admins",
json={"username": "bad_role_user", "password": "pass1234", "role": "不存在的角色"},
headers=_auth(super_token),
)
assert r.status_code == 400