Files
shaguabijia-app-server/tests/test_admin_read.py
T
OuYingJun1024 995908cd0b feat(rewards/admin): LT 因子改账号累计 + 审计 only_mismatch + 用户金币现金 set 模式
- 看视频奖励 LT 因子(因子2)按「账号累计第 N 次看视频」递减,不再按天重置:
  ad_reward 新增 _granted_cumulative、ad_feed_reward 单位序号去掉当天过滤、
  rewards 形参 today_count_after_this → count_after_this;每日次数上限/冷却仍按当日统计
- 广告金币审计:加 only_mismatch 筛选只看 ✗ 行;total/mismatch_count 改全量统计
  (不受 limit/筛选影响、截断前算)+ 新增 truncated 标记展示集是否被截断
- admin 用户金币/现金接口加 mode=delta|set:set=设为目标值(读余额算差值、仍写一笔流水,
  沿用原子/审计/扣负保护);新增 admin-user-cash 文档 + 更新 API 索引/coins 等文档 + 补 admin read/write 测试
2026-06-13 21:26:11 +08:00

184 lines
6.9 KiB
Python

"""Admin M2 读接口测试:大盘聚合 + 用户/流水/提现/反馈列表 + 鉴权拦截。"""
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.db.session import SessionLocal
from app.models.feedback import Feedback
from app.models.wallet import CashTransaction, WithdrawOrder
from app.repositories import user as user_repo
from app.repositories import wallet as wallet_repo
@pytest.fixture()
def admin_client() -> TestClient:
return TestClient(admin_app)
@pytest.fixture()
def admin_token() -> str:
db = SessionLocal()
try:
if admin_repo.get_by_username(db, "m2_admin") is None:
admin_repo.create_admin(db, username="m2_admin", password="m2-pass", role="super_admin")
finally:
db.close()
c = TestClient(admin_app)
r = c.post("/admin/api/auth/login", json={"username": "m2_admin", "password": "m2-pass"})
return r.json()["access_token"]
def _auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
def _seed_user_with_data(phone: str) -> int:
"""造一个用户 + 金币流水 + 现金流水 + 提现单 + 反馈,返回 user_id。"""
db = SessionLocal()
try:
user = user_repo.upsert_user_for_login(db, phone=phone, register_channel="sms")
uid = user.id
wallet_repo.grant_coins(db, uid, 5000, biz_type="signin", remark="测试发金币")
db.commit()
db.add(CashTransaction(
user_id=uid, amount_cents=-100, balance_after_cents=0, biz_type="withdraw", remark="t"
))
db.add(WithdrawOrder(
user_id=uid, out_bill_no=f"test{uid}billno0001", amount_cents=100, status="success"
))
db.add(Feedback(user_id=uid, content="测试反馈内容", contact="wx_test", status="new"))
db.commit()
return uid
finally:
db.close()
def test_dashboard_overview(admin_client: TestClient, admin_token: str) -> None:
_seed_user_with_data("13800000001")
r = admin_client.get("/admin/api/stats/overview", headers=_auth(admin_token))
assert r.status_code == 200, r.text
data = r.json()
assert data["users"]["total"] >= 1
assert data["coins"]["granted_total"] >= 5000
assert "success_rate" in data["comparison"]
assert data["cps"]["available"] is False
def test_user_list_and_detail(admin_client: TestClient, admin_token: str) -> None:
uid = _seed_user_with_data("13800000002")
r = admin_client.get("/admin/api/users", headers=_auth(admin_token))
assert r.status_code == 200, r.text
assert "items" in r.json()
r = admin_client.get(f"/admin/api/users/{uid}", headers=_auth(admin_token))
assert r.status_code == 200, r.text
d = r.json()
assert d["user"]["id"] == uid
assert d["coin_balance"] == 5000
assert d["withdraw_total"] >= 1
assert d["feedback_total"] >= 1
assert admin_client.get("/admin/api/users/999999", headers=_auth(admin_token)).status_code == 404
def test_user_filter_by_status(admin_client: TestClient, admin_token: str) -> None:
_seed_user_with_data("13800000003")
r = admin_client.get("/admin/api/users", params={"status": "active"}, headers=_auth(admin_token))
assert r.status_code == 200
assert all(u["status"] == "active" for u in r.json()["items"])
def test_wallet_and_withdraw_lists(admin_client: TestClient, admin_token: str) -> None:
uid = _seed_user_with_data("13800000004")
r = admin_client.get(
"/admin/api/wallet/coin-transactions", params={"user_id": uid}, headers=_auth(admin_token)
)
assert r.status_code == 200
assert len(r.json()["items"]) >= 1
r = admin_client.get("/admin/api/withdraws", params={"user_id": uid}, headers=_auth(admin_token))
assert r.status_code == 200
assert len(r.json()["items"]) >= 1
assert all(o["status"] == "success" for o in r.json()["items"])
def test_feedback_list(admin_client: TestClient, admin_token: str) -> None:
_seed_user_with_data("13800000005")
r = admin_client.get("/admin/api/feedbacks", params={"status": "new"}, headers=_auth(admin_token))
assert r.status_code == 200
assert all(f["status"] == "new" for f in r.json()["items"])
def test_ad_coin_audit_full_count_truncate_and_only_mismatch(
admin_client: TestClient, admin_token: str
) -> None:
"""A+B:total/mismatch_count 按全量统计(不受 limit 影响),truncated 旗标 + only_mismatch 过滤。
用 capped 行造确定性数据(应发恒 0,coin==0 即一致),不依赖发奖公式:
3 条 coin=0(一致) + 2 条 coin=7(不一致) → 全量 total=5、mismatch=2。
"""
from app.models.ad_reward import AdRewardRecord
d = "2020-01-15" # 固定历史日 + 独立 user,隔离其它用例数据
db = SessionLocal()
try:
uid = user_repo.upsert_user_for_login(db, phone="13800009999", register_channel="sms").id
for i in range(3):
db.add(AdRewardRecord(
trans_id=f"adaudit-ok-{i}", user_id=uid, coin=0, status="capped",
reward_scene="reward_video", reward_date=d,
))
for i in range(2):
db.add(AdRewardRecord(
trans_id=f"adaudit-bad-{i}", user_id=uid, coin=7, status="capped",
reward_scene="reward_video", reward_date=d,
))
db.commit()
finally:
db.close()
base = {"date": d, "user_id": uid}
# 全量:total=5、mismatch=2、不截断、返回 5 条
body = admin_client.get(
"/admin/api/ad-coin-audit", params={**base, "limit": 100}, headers=_auth(admin_token)
).json()
assert body["total"] == 5
assert body["mismatch_count"] == 2
assert body["truncated"] is False
assert len(body["items"]) == 5
# 截断:limit=2 → 统计仍全量、truncated=True、只回 2 条
body = admin_client.get(
"/admin/api/ad-coin-audit", params={**base, "limit": 2}, headers=_auth(admin_token)
).json()
assert body["total"] == 5 and body["mismatch_count"] == 2
assert body["truncated"] is True
assert len(body["items"]) == 2
# only_mismatch:只回 ✗ 行(2 条),统计仍全量、不截断
body = admin_client.get(
"/admin/api/ad-coin-audit",
params={**base, "limit": 100, "only_mismatch": True},
headers=_auth(admin_token),
).json()
assert body["total"] == 5 and body["mismatch_count"] == 2
assert body["truncated"] is False
assert len(body["items"]) == 2
assert all(it["matched"] is False for it in body["items"])
def test_read_apis_require_auth(admin_client: TestClient) -> None:
"""所有 M2 读接口未带 token → 401(router 级 get_current_admin 守卫)。"""
for path in [
"/admin/api/stats/overview",
"/admin/api/users",
"/admin/api/wallet/coin-transactions",
"/admin/api/withdraws",
"/admin/api/feedbacks",
]:
assert admin_client.get(path).status_code == 401, path