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>
This commit was merged in pull request #147.
This commit is contained in:
@@ -123,7 +123,8 @@ def test_builtin_roles_labels_and_pages(admin_client, super_token) -> None:
|
||||
# 页集对齐 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",
|
||||
"dashboard", "device-liveness", "config", "ad-revenue", "huawei-review",
|
||||
"event-logs", "audit-logs",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""华为审核开关: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
|
||||
Reference in New Issue
Block a user