[阶段3.1] pytest 测试基建 + auth/health/analysis 单元测试 - 102 用例全绿
测试基建: - pytest.ini / requirements-dev.txt - tests/conftest.py: 每个用例独立 SQLite 库,提供 db / app / client / user / auth / seed_health 六个 fixture tests/test_auth.py (38 通过, 1 跳过): - 密码哈希: 加盐唯一性、格式自描述、明文不入库、畸形哈希不抛异常 - 回归用例: 无 scrypt 的解释器上也能哈希(覆盖上一个 commit 的 bug) - JWT: 过期/换密钥/篡改签名均拒绝 - 登录错误不区分"邮箱不存在"与"密码错误"(防用户枚举) - require_auth: 缺失/畸形/过期 header 一律 401 而非 500 tests/test_health.py (30 通过): - 日期范围上下界均为闭区间 - 数据按 user_id 隔离,查不到他人数据 - 重复 upsert 同一天不产生重复行 - 各指标端点正确剔除 NULL 行 tests/test_analysis.py (34 通过): - 5 条建议规则的阈值边界逐条固化(8000 步 / 7 小时 / 50 压力 / 65 静息心率 / 40 HRV) - 均值按窗口计算而非逐日;只取最近 14 天 - 无睡眠记录的日子不被当作 0 拉低均值 - metric 名走白名单,SQL 注入串不生效 - 建议按 high/medium/low 排序 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
98
backend/tests/conftest.py
Normal file
98
backend/tests/conftest.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Shared pytest fixtures.
|
||||
|
||||
Environment must be configured BEFORE `config` is imported, because config.py
|
||||
reads os.environ at import time. Each test then gets its own SQLite file so
|
||||
tests never share state.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, _BACKEND_DIR)
|
||||
|
||||
os.environ.setdefault("DB_TYPE", "sqlite")
|
||||
os.environ.setdefault("JWT_SECRET", "test_secret_at_least_32_bytes_long_ok")
|
||||
os.environ.setdefault("CORS_ORIGIN", "http://localhost:3000")
|
||||
# Point at a throwaway path; the db_path fixture overrides it per test.
|
||||
os.environ.setdefault(
|
||||
"DATABASE_PATH", os.path.join(tempfile.mkdtemp(), "bootstrap.db")
|
||||
)
|
||||
|
||||
import db as db_module # noqa: E402
|
||||
from app import create_app # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path, monkeypatch):
|
||||
"""A freshly initialized, isolated SQLite database for one test."""
|
||||
path = str(tmp_path / "test.db")
|
||||
monkeypatch.setattr(db_module, "SQLITE_PATH", path)
|
||||
db_module.init_db()
|
||||
return db_module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(db):
|
||||
application = create_app()
|
||||
application.config.update(TESTING=True)
|
||||
return application
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(client):
|
||||
"""A registered user: returns {id, email, token, password}."""
|
||||
password = "secret123"
|
||||
resp = client.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": "tester@example.com",
|
||||
"garminEmail": "gm@example.com",
|
||||
"garminPassword": password,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 201, resp.get_data(as_text=True)
|
||||
body = resp.get_json()
|
||||
return {**body, "password": password}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth(user):
|
||||
"""Authorization headers for the registered user."""
|
||||
return {"Authorization": f"Bearer {user['token']}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed_health(db, user):
|
||||
"""Insert daily health rows. Returns the inserted records."""
|
||||
|
||||
def _seed(records):
|
||||
for r in records:
|
||||
db.execute(
|
||||
"INSERT INTO health_data (id, user_id, date, steps, heart_rate, "
|
||||
"heart_rate_variability, sleep_duration, sleep_quality, stress, "
|
||||
"calories_burned) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
[
|
||||
f"{user['id']}-{r['date']}",
|
||||
user["id"],
|
||||
r["date"],
|
||||
r.get("steps"),
|
||||
r.get("heart_rate"),
|
||||
r.get("hrv"),
|
||||
r.get("sleep_duration"),
|
||||
r.get("sleep_quality"),
|
||||
r.get("stress"),
|
||||
r.get("calories"),
|
||||
],
|
||||
)
|
||||
return records
|
||||
|
||||
return _seed
|
||||
Reference in New Issue
Block a user