b50495bebe
短信(SMS_MOCK 切 mock/real): - integrations/sms.py 重写: real 模式走极光短信 REST /v1/messages 自定义验证码(本服务 secrets 生成 6 位码 + 进程内存 + 本地校验一次性/防爆破), 鉴权复用极光一键登录 JG_APP_KEY/MASTER_SECRET (同一极光应用, 上线只需 SMS_MOCK=false); mock 仍"任意6位通过"不动其余测试 - 防刷四层: 单号冷却 + 单号每日上限 + 单IP rate_limit(/sms/send 10/min、/sms/login 20/min) + 单码失败次数作废; SmsError 带 status_code 映射 429/503/400 - config 增 SMS_SEND_ENDPOINT/SIGN_ID/TEMPLATE_ID/CODE_LENGTH/DAILY_LIMIT/MAX_VERIFY_ATTEMPTS; test_auth 加 real 模式单测; sms.md/后端技术实现/待办账本同步 admin 后台(app/admin/ 独立子应用, uvicorn app.admin.main:admin_app :8771): - 复用主仓 models/repositories/integrations + 同库, 鉴权完全隔离(ADMIN_JWT_SECRET≠JWT_SECRET_KEY + payload typ=admin + bcrypt 密码 + 可选 IP 白名单); 主 app 不 import 本包, admin 崩不影响主进程 - 路由: 登录 / 账号管理(RBAC: super_admin·finance·operator) / 用户列表+360详情+封禁+手动调币 / 钱包流水 / 提现重试对账 / 反馈工单 / 数据大盘; 全写操作落 admin_audit_log(涉钱与业务写同事务) - 涉钱逻辑(调微信/退款/对账)复用 app.repositories.wallet 不重写 - 新增 models/admin.py(AdminUser/AdminAuditLog) + admin_tables 迁移 + create_admin.py + deploy/shaguabijia-admin.service; 依赖加 bcrypt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
4.6 KiB
Python
125 lines
4.6 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_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
|