Files
GarminHealthLab/backend/tests/conftest.py
ericwyuan 5f07dad019 [阶段5] 部署到 NAS + frp 公网映射,并加注册锁
部署 (NAS 192.168.50.64):
- MariaDB 建库 garmin_health_lab,5 张表由 init_db 建好
- Python 3.8.15 venv;NAS 无 gcc,依赖全部走纯 Python 轮子
- gunicorn 2 worker × 4 线程,--timeout 300(AI 生成耗时可达数分钟)
- start.sh / stop.sh,可重复执行;日志落 logs/
- 在 NAS 真机 + 真实 MariaDB 上跑通全部测试:205 passed

app.py / config.py:
- STATIC_DIR 存在时由同一个 Flask 进程托管 React 构建产物,
  部署即单端口单进程,不需要额外反代
- 404 处理区分 /api 前缀:API 仍返回 JSON,其余回退到 index.html,
  这样 /settings 这类前端路由刷新后不会 404

安全 - 注册锁 (ALLOW_REGISTRATION):
- 服务要挂到公网,而原本 /register 完全开放,任何人都能注册进来
  读取健康数据
- 默认策略 auto:仅在尚无任何账号时开放,注册完第一个即自动关闭
- 另支持 true / false 显式覆盖;按请求读取,改配置无需重启
- 新增 GET /auth/registration-status,前端据此隐藏注册标签页

frp 公网映射:
- 复用 NAS 上已有的 frpc (/etc/frp/frpc.toml),追加 garmin 隧道
  NAS:8123 -> 甲骨文:8123(改前已按既有惯例备份 .bak.<时间戳>)
- 经 S99frpc.sh restart 生效,原有 4 条隧道均正常恢复

tests/test_registration_policy.py (13 通过):
- auto 策略下第一个账号放行、第二个 403 且不落库
- true/false 显式覆盖,大小写不敏感
- 策略按请求读取而非 import 时冻结
- 关闭注册不影响登录;status 端点无需鉴权

公网实测: 页面、SPA 路由、鉴权 401、注册锁 403 均符合预期。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 18:49:05 +08:00

127 lines
3.7 KiB
Python

"""
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
# config.py calls load_dotenv() at import, so backend/.env leaks into the test
# process — a developer's real AI_MODEL_CHAIN or API keys would silently change
# what the suite exercises (and could bill real API calls). Clear them here;
# individual tests opt back in through the `keys` / `gateway` fixtures.
_AI_ENV_VARS = (
"AI_MODEL_CHAIN",
"AI_DAY_BUDGET",
"AI_TIMEOUT_SECONDS",
"GEMINI_API_KEY",
"NVIDIA_API_KEY",
"NVIDIA_BASE_URL",
"AI_GATEWAY_TOKEN",
"AI_GATEWAY_BASE_URL",
"AI_GATEWAY_MODEL",
"OLLAMA_BASE_URL",
"OLLAMA_MODEL",
)
@pytest.fixture(autouse=True)
def _isolate_ai_env(monkeypatch):
for var in _AI_ENV_VARS:
monkeypatch.delenv(var, raising=False)
# Most tests need to create users freely; the production default closes
# registration once one account exists. test_registration_policy.py clears
# this to exercise the real default.
monkeypatch.setenv("ALLOW_REGISTRATION", "true")
@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