Files
shaguabijia-app-server/tests/test_huawei_review.py
T
zuochenyong c53ce896f7 feat(huawei-review): 华为审核开关(admin 可切 + 客户端下发 + 审计) (#147)
华为应用市场审核要求新手引导的「快速设置」权限步必须可被用户关闭,平时又要
保住权限开启率,故做成后台可切的两态开关,送审期间切开、过审后收回。

- app_config 新增 huawei_review 行(default / review),空库与脏值一律回退
  default = 上线至今的现状,宁可不给退出按钮也不误放开
- admin: GET/PATCH /admin/api/huawei-review,权限 operator/tech,切换写审计
- 客户端: GET /api/v1/platform/huawei-review 不鉴权(引导页在登录前就展示),
  下发 onboarding_closable;机型 gate 由客户端做,故此处不判 ROM

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: 左辰勇 <exinglang@gmail.com>
Reviewed-on: #147
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
2026-07-21 10:43:58 +08:00

131 lines
4.6 KiB
Python

"""华为审核开关:admin 读写 + 客户端公开端点 + 审计 + 空库回退。
背景:华为应用市场审核要求新手引导的「快速设置」权限步必须可被用户关闭。开关切到 review 后
客户端(仅华为 ROM)在该步左上角显示退出按钮。默认 default = 上线至今的现状(不可关闭)。
autouse 清理每个用例后清空 app_config,避免污染其他文件里假设默认值的用例(同 test_admin_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
@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, "hw_admin") is None:
admin_repo.create_admin(
db, username="hw_admin", password="hwpass123", role="super_admin"
)
finally:
db.close()
c = TestClient(admin_app)
return c.post(
"/admin/api/auth/login", json={"username": "hw_admin", "password": "hwpass123"}
).json()["access_token"]
@pytest.fixture(autouse=True)
def _clean_config() -> Iterator[None]:
yield
db = SessionLocal()
try:
db.execute(delete(AppConfig))
# 审计行同样要清:同库跨用例累积会让 test_switch_writes_audit 数到前面用例写的行
# (按 action 限定,不碰其他模块可能已写入的审计)。
db.execute(delete(AdminAuditLog).where(AdminAuditLog.action == "huawei_review.set"))
db.commit()
finally:
db.close()
def _auth(t: str) -> dict:
return {"Authorization": f"Bearer {t}"}
def test_public_default_not_closable(client: TestClient) -> None:
"""空库(从未切过)→ 客户端拿到 default / 不可关闭 = 维持现状;且不需要鉴权。"""
r = client.get("/api/v1/platform/huawei-review")
assert r.status_code == 200, r.text
assert r.json() == {"mode": "default", "onboarding_closable": False}
def test_admin_get_default(admin_client: TestClient, token: str) -> None:
r = admin_client.get("/admin/api/huawei-review", headers=_auth(token))
assert r.status_code == 200, r.text
body = r.json()
assert body["mode"] == "default"
assert body["updated_at"] is None # 从未切过
def test_switch_to_review_takes_effect(
admin_client: TestClient, client: TestClient, token: str
) -> None:
"""admin 切 review → 公开端点立刻下发可关闭(跨进程/跨 app 一致,因为落在 DB 而非内存)。"""
r = admin_client.patch(
"/admin/api/huawei-review", json={"mode": "review"}, headers=_auth(token)
)
assert r.status_code == 200, r.text
assert r.json()["mode"] == "review"
assert r.json()["updated_at"] is not None
pub = client.get("/api/v1/platform/huawei-review").json()
assert pub == {"mode": "review", "onboarding_closable": True}
# 切回 default → 客户端恢复不可关闭(审核过了要能一键收回)
admin_client.patch(
"/admin/api/huawei-review", json={"mode": "default"}, headers=_auth(token)
)
assert client.get("/api/v1/platform/huawei-review").json()["onboarding_closable"] is False
def test_switch_writes_audit(admin_client: TestClient, token: str) -> None:
admin_client.patch(
"/admin/api/huawei-review", json={"mode": "review"}, headers=_auth(token)
)
db = SessionLocal()
try:
logs = db.execute(
select(AdminAuditLog).where(AdminAuditLog.action == "huawei_review.set")
).scalars().all()
assert len(logs) == 1
assert logs[0].detail == {"before": "default", "after": "review"}
finally:
db.close()
def test_invalid_mode_rejected(admin_client: TestClient, token: str) -> None:
"""mode 是 Literal,非法值由 FastAPI 校验挡在 422(不会落库)。"""
r = admin_client.patch(
"/admin/api/huawei-review", json={"mode": "nope"}, headers=_auth(token)
)
assert r.status_code == 422, r.text
db = SessionLocal()
try:
assert db.get(AppConfig, "huawei_review") is None
finally:
db.close()
def test_requires_admin_auth(admin_client: TestClient) -> None:
assert admin_client.get("/admin/api/huawei-review").status_code == 401
assert admin_client.patch(
"/admin/api/huawei-review", json={"mode": "review"}
).status_code == 401