15fb73791f
## 需求背景 将比价、短信与登录、广告、引导与账号、风控免告警等限制统一配置,并支持按手机号或设备设置有有效期的临时白名单。 ## 主要改动 - 新增统一限制策略注册表、全局 JSON 配置与白名单覆盖表 - 新增白名单管理、设备检索、批量追加与主体统一编辑接口 - 接入比价、短信登录、广告奖励、引导视频、账号换绑及风险告警调用链 - 保留旧配置接口兼容,并同步统一策略全局值 - 增加单主体唯一有效期、恢复全局、审计日志和风险事件自动处理 - 增加数据库迁移及完整回归测试 ## 验证 - 白名单、权限、配置及风控测试 50 项通过 - 短信、登录、比价、广告关联测试 98 项通过 - Ruff 与 Python 编译检查通过 - Alembic 保持单一 head - 已同步最新 main --------- Co-authored-by: unknown <798648091@qq.com> Reviewed-on: #207 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
218 lines
6.9 KiB
Python
218 lines
6.9 KiB
Python
"""Admin M5 配置后台化测试:列出/改配置 + 改配**真生效** + 校验 + 审计。
|
|
|
|
autouse 清理每个用例后清空 app_config,避免改配污染其他文件的福利测试。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import delete, select
|
|
|
|
from app.admin.main import admin_app
|
|
from app.admin.repositories import admin_user as admin_repo
|
|
from app.db.session import SessionLocal
|
|
from app.models.admin import AdminAuditLog
|
|
from app.models.app_config import AppConfig
|
|
from app.repositories import ad_reward, signin
|
|
from app.repositories import user as user_repo
|
|
|
|
|
|
@pytest.fixture()
|
|
def admin_client() -> TestClient:
|
|
return TestClient(admin_app)
|
|
|
|
|
|
@pytest.fixture()
|
|
def token() -> str:
|
|
db = SessionLocal()
|
|
try:
|
|
if admin_repo.get_by_username(db, "cfg_admin") is None:
|
|
admin_repo.create_admin(
|
|
db, username="cfg_admin", password="cfgpass12", role="super_admin"
|
|
)
|
|
finally:
|
|
db.close()
|
|
c = TestClient(admin_app)
|
|
return c.post(
|
|
"/admin/api/auth/login", json={"username": "cfg_admin", "password": "cfgpass12"}
|
|
).json()["access_token"]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_config() -> Iterator[None]:
|
|
"""每个用例后清空 app_config,避免改配污染其他文件的福利测试(它们假设默认值)。"""
|
|
yield
|
|
db = SessionLocal()
|
|
try:
|
|
db.execute(delete(AppConfig))
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _auth(t: str) -> dict:
|
|
return {"Authorization": f"Bearer {t}"}
|
|
|
|
|
|
def _seed_user(phone: str) -> int:
|
|
db = SessionLocal()
|
|
try:
|
|
return user_repo.upsert_user_for_login(db, phone=phone, register_channel="sms").id
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_list_config(admin_client: TestClient, token: str) -> None:
|
|
r = admin_client.get("/admin/api/config", headers=_auth(token))
|
|
assert r.status_code == 200, r.text
|
|
items = {i["key"]: i for i in r.json()}
|
|
# 非 hidden 项照常返回;广告次数上限迁到「白名单」统一配置。
|
|
assert "signin_rewards" in items and "ad_cooldown_sec" in items
|
|
# hidden 项(任务/里程碑、首页轮播数据源、广告次数/单次金币/每轮次数/信息流广告开关)不在配置页返回。
|
|
for hidden_key in (
|
|
"task_rewards", "record_milestones", "marquee_feed_mode",
|
|
"ad_daily_limit", "ad_reward_coin", "ad_round_count", "comparing_ad_enabled",
|
|
):
|
|
assert hidden_key not in items, f"{hidden_key} 应被 hidden 过滤"
|
|
assert items["signin_rewards"]["value"] == [
|
|
200, 200, 300, 200, 400, 400, 800,
|
|
]
|
|
assert items["signin_rewards"]["overridden"] is False
|
|
|
|
|
|
def test_update_signin_takes_effect(admin_client: TestClient, token: str) -> None:
|
|
r = admin_client.patch(
|
|
"/admin/api/config/signin_rewards",
|
|
json={"value": [100, 200, 300, 400, 500, 600, 700]},
|
|
headers=_auth(token),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["value"][0] == 100 and r.json()["overridden"] is True
|
|
|
|
# 业务真的用上新值(配置后台化的核心验证)
|
|
uid = _seed_user("13912340001")
|
|
db = SessionLocal()
|
|
try:
|
|
st = signin.get_status(db, uid)
|
|
assert st.steps[0].coin == 100
|
|
assert st.today_coin == 100 # 首签=第 1 天=100(新值)
|
|
logs = db.execute(
|
|
select(AdminAuditLog).where(
|
|
AdminAuditLog.action == "config.set",
|
|
AdminAuditLog.target_id == "signin_rewards",
|
|
)
|
|
).scalars().all()
|
|
assert len(logs) == 1 and logs[0].detail["after"][0] == 100
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_update_ad_limit_takes_effect(admin_client: TestClient, token: str) -> None:
|
|
r = admin_client.patch(
|
|
"/admin/api/config/ad_daily_limit", json={"value": 5}, headers=_auth(token)
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
uid = _seed_user("13912340002")
|
|
db = SessionLocal()
|
|
try:
|
|
# today_status 现返 7 元组(末两位为观看时长闸:已看秒数 / 上限),取次数上限位
|
|
_used, limit, _coin, _rc, _cd, _ws, _wl = ad_reward.today_status(db, uid)
|
|
assert limit == 5 # 新配置生效
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_list_config_reads_limit_values_from_global_bundle(
|
|
admin_client: TestClient,
|
|
token: str,
|
|
) -> None:
|
|
changed = admin_client.patch(
|
|
"/admin/api/config/ad_cooldown_sec",
|
|
json={"value": 17},
|
|
headers=_auth(token),
|
|
)
|
|
assert changed.status_code == 200, changed.text
|
|
|
|
items = {
|
|
item["key"]: item
|
|
for item in admin_client.get(
|
|
"/admin/api/config",
|
|
headers=_auth(token),
|
|
).json()
|
|
}
|
|
assert items["ad_cooldown_sec"]["value"] == 17
|
|
assert items["ad_cooldown_sec"]["overridden"] is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "value"),
|
|
[
|
|
("ad_daily_limit", 0),
|
|
("ad_daily_limit", 100_001),
|
|
("ad_cooldown_sec", 86_401),
|
|
],
|
|
)
|
|
def test_update_limit_config_rejects_out_of_range_values(
|
|
admin_client: TestClient,
|
|
token: str,
|
|
key: str,
|
|
value: int,
|
|
) -> None:
|
|
response = admin_client.patch(
|
|
f"/admin/api/config/{key}",
|
|
json={"value": value},
|
|
headers=_auth(token),
|
|
)
|
|
assert response.status_code == 400, response.text
|
|
|
|
|
|
def test_update_bool_config(admin_client: TestClient, token: str) -> None:
|
|
# 提现自动对账开关默认 True
|
|
items = {
|
|
i["key"]: i
|
|
for i in admin_client.get("/admin/api/config", headers=_auth(token)).json()
|
|
}
|
|
assert items["withdraw_auto_reconcile_enabled"]["type"] == "bool"
|
|
assert items["withdraw_auto_reconcile_enabled"]["value"] is True
|
|
|
|
# 关掉 → DB 落 False、业务读到 False
|
|
r = admin_client.patch(
|
|
"/admin/api/config/withdraw_auto_reconcile_enabled",
|
|
json={"value": False},
|
|
headers=_auth(token),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["value"] is False and r.json()["overridden"] is True
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
from app.repositories import app_config
|
|
|
|
assert app_config.get_value(db, "withdraw_auto_reconcile_enabled") is False
|
|
finally:
|
|
db.close()
|
|
|
|
# bool 项不接受非布尔值
|
|
assert admin_client.patch(
|
|
"/admin/api/config/withdraw_auto_reconcile_enabled",
|
|
json={"value": 1},
|
|
headers=_auth(token),
|
|
).status_code == 400
|
|
|
|
|
|
def test_config_validation(admin_client: TestClient, token: str) -> None:
|
|
# 签到档位长度≠7
|
|
assert admin_client.patch(
|
|
"/admin/api/config/signin_rewards", json={"value": [1, 2, 3]}, headers=_auth(token)
|
|
).status_code == 400
|
|
# int 负数
|
|
assert admin_client.patch(
|
|
"/admin/api/config/ad_daily_limit", json={"value": -5}, headers=_auth(token)
|
|
).status_code == 400
|
|
# 未知 key
|
|
assert admin_client.patch(
|
|
"/admin/api/config/nope", json={"value": 1}, headers=_auth(token)
|
|
).status_code == 404
|