Files
shaguabijia-app-server/tests/test_device_profile.py
T
unknown afc55ab910 feat(device): 新增设备信息表 device + 上报接口
- 新建 device 设备表(设备/系统信息 + 网络/时区/模拟器/位置 + 关联用户 + 状态/时间戳),
  以客户端 device_id 唯一标识;登录后回填 user_id,游客态可空
- 原 device 模型改名 device_liveness(实为无障碍存活表),腾出 device 名给新表并保持
  表名与实体名一致;同步更新 __init__ / repositories / admin queries / 文档引用
- 新增 POST /api/v1/device/report:软鉴权(带 JWT 绑用户、游客也收),按 device_id upsert;
  设备信息非空才覆盖,位置 latitude/longitude 整字段覆盖含清空(以本次上报实际为准)
- 新增 alembic migration bb47051068c8 建 device 表(裁掉 autogenerate 带出的无关表漂移)
- 补 /device/report 端点与 upsert 单元测试 4 例
2026-07-17 09:46:59 +08:00

99 lines
3.2 KiB
Python

"""设备档案上报端点 /api/v1/device/report + upsert 测试。"""
from __future__ import annotations
from sqlalchemy import select
from app.core.security import create_token
from app.db.session import SessionLocal
from app.models.device import Device
from app.models.user import User
def _payload(**over) -> dict:
base = {
"device_id": "dev-report-1",
"platform": "android",
"oem": "Xiaomi",
"model": "PJF110",
"os_version": "Android 14",
"app_version": "0.2.12(62)",
"channel": "yingyongbao",
"screen": "1080x2400",
"network": "wifi",
"timezone": "Asia/Shanghai",
"is_emulator": False,
"latitude": 31.23,
"longitude": 121.47,
}
base.update(over)
return base
def _get(device_id: str) -> Device | None:
with SessionLocal() as db:
return db.execute(
select(Device).where(Device.device_id == device_id)
).scalar_one_or_none()
def test_report_creates_device_as_guest(client) -> None:
r = client.post("/api/v1/device/report", json=_payload(device_id="dev-guest"))
assert r.status_code == 200, r.text
assert r.json()["ok"] is True
d = _get("dev-guest")
assert d is not None
assert d.user_id is None # 游客态:未绑用户
assert d.oem == "Xiaomi"
assert d.is_emulator is False
assert d.latitude == 31.23
assert d.last_active_at is not None
def test_report_binds_user_when_authed(client) -> None:
with SessionLocal() as db:
u = User(phone="13800000001", username="20000000001")
db.add(u)
db.commit()
uid = u.id
token, _ = create_token(user_id=uid, token_type="access")
r = client.post(
"/api/v1/device/report",
json=_payload(device_id="dev-user"),
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 200, r.text
assert _get("dev-user").user_id == uid
def test_report_clears_location_when_absent(client) -> None:
# 首次带位置
client.post(
"/api/v1/device/report",
json=_payload(device_id="dev-loc", latitude=10.0, longitude=20.0),
)
assert _get("dev-loc").latitude == 10.0
# 再次上报拿不到位置(lat/lng=None)→ 必须清空,不能保留旧坐标
r = client.post(
"/api/v1/device/report",
json=_payload(device_id="dev-loc", latitude=None, longitude=None, oem="HONOR"),
)
assert r.status_code == 200, r.text
d = _get("dev-loc")
assert d.latitude is None # 位置以实际为准 → 清空
assert d.longitude is None
assert d.oem == "HONOR" # 设备信息正常更新
def test_report_sticky_fields_not_wiped_by_missing(client) -> None:
# 首次全量
client.post("/api/v1/device/report", json=_payload(device_id="dev-sticky"))
# 再次只带 device_id + 位置(不带 oem/model)→ 设备信息保留旧值(非空才覆盖)
client.post(
"/api/v1/device/report",
json={"device_id": "dev-sticky", "latitude": 1.0, "longitude": 2.0},
)
d = _get("dev-sticky")
assert d.oem == "Xiaomi" # 未被漏带的 None 冲掉
assert d.model == "PJF110"
assert d.latitude == 1.0 # 位置按本次