2026-01-22 12:57:26 +08:00
|
|
|
"""Web API 测试"""
|
|
|
|
|
|
|
|
|
|
from datetime import date, timedelta
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
from src.vitals.core import database as db
|
|
|
|
|
from src.vitals.core.models import Exercise, Meal, Sleep, Weight, UserConfig
|
2026-01-27 16:16:32 +08:00
|
|
|
from src.vitals.core.auth import create_token
|
2026-01-22 12:57:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def client():
|
2026-01-27 16:16:32 +08:00
|
|
|
"""创建测试客户端(带认证)"""
|
2026-01-22 12:57:26 +08:00
|
|
|
from src.vitals.web.app import app
|
2026-01-27 16:16:32 +08:00
|
|
|
|
|
|
|
|
# 确保有测试用户
|
|
|
|
|
active_user = db.get_active_user()
|
|
|
|
|
if not active_user:
|
|
|
|
|
# 获取或创建默认用户
|
|
|
|
|
users = db.get_users()
|
|
|
|
|
if users:
|
|
|
|
|
active_user = users[0]
|
|
|
|
|
db.set_active_user(active_user.id)
|
|
|
|
|
else:
|
|
|
|
|
# 创建默认测试用户
|
|
|
|
|
from src.vitals.core.auth import hash_password
|
|
|
|
|
user_id = db.create_user("test_user", hash_password("test123"))
|
|
|
|
|
db.set_active_user(user_id)
|
|
|
|
|
active_user = db.get_user_by_id(user_id)
|
|
|
|
|
|
|
|
|
|
# 创建认证 token
|
|
|
|
|
token = create_token(active_user.id, active_user.name, active_user.is_admin)
|
|
|
|
|
|
|
|
|
|
# 创建带认证 cookie 的客户端
|
|
|
|
|
test_client = TestClient(app)
|
|
|
|
|
test_client.cookies.set("auth_token", token)
|
|
|
|
|
return test_client
|
2026-01-22 12:57:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def populated_db():
|
|
|
|
|
"""填充测试数据"""
|
|
|
|
|
today = date.today()
|
|
|
|
|
|
|
|
|
|
# 用户配置
|
|
|
|
|
config = UserConfig(
|
|
|
|
|
age=28, gender="male", height=175.0, weight=72.0,
|
|
|
|
|
activity_level="moderate", goal="maintain",
|
|
|
|
|
)
|
2026-01-27 17:39:24 +08:00
|
|
|
db.save_config(1, config)
|
2026-01-22 12:57:26 +08:00
|
|
|
|
|
|
|
|
# 今日数据
|
|
|
|
|
db.add_exercise(Exercise(
|
|
|
|
|
date=today, type="跑步", duration=30, calories=240, distance=5.0,
|
|
|
|
|
))
|
|
|
|
|
db.add_meal(Meal(
|
|
|
|
|
date=today, meal_type="午餐",
|
|
|
|
|
description="米饭+鸡肉", calories=500,
|
|
|
|
|
))
|
|
|
|
|
db.add_meal(Meal(
|
|
|
|
|
date=today, meal_type="早餐",
|
|
|
|
|
description="燕麦+鸡蛋", calories=350,
|
|
|
|
|
))
|
|
|
|
|
db.add_sleep(Sleep(
|
|
|
|
|
date=today, duration=7.5, quality=4,
|
|
|
|
|
))
|
|
|
|
|
db.add_weight(Weight(
|
|
|
|
|
date=today, weight_kg=72.5, body_fat_pct=18.5,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return today
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRootEndpoint:
|
|
|
|
|
"""根路径测试"""
|
|
|
|
|
|
|
|
|
|
def test_root_returns_html(self, client):
|
|
|
|
|
"""测试返回 HTML"""
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "text/html" in response.headers["content-type"]
|
|
|
|
|
|
|
|
|
|
def test_root_contains_dashboard(self, client):
|
|
|
|
|
"""测试包含仪表盘内容"""
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert "Vitals" in response.text
|
|
|
|
|
assert "健康管理" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfigEndpoint:
|
|
|
|
|
"""配置接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_config(self, client, populated_db):
|
|
|
|
|
"""测试获取配置"""
|
|
|
|
|
response = client.get("/api/config")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["age"] == 28
|
|
|
|
|
assert data["gender"] == "male"
|
|
|
|
|
assert data["bmr"] is not None
|
|
|
|
|
assert data["tdee"] is not None
|
|
|
|
|
|
|
|
|
|
def test_get_config_empty(self, client):
|
|
|
|
|
"""测试空配置"""
|
|
|
|
|
response = client.get("/api/config")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["activity_level"] == "moderate"
|
|
|
|
|
|
2026-01-27 17:39:24 +08:00
|
|
|
def test_update_config_target_weight(self, client, populated_db):
|
|
|
|
|
"""测试更新目标体重"""
|
|
|
|
|
response = client.post("/api/config", json={"target_weight": 65.0})
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["target_weight"] == 65.0
|
|
|
|
|
|
|
|
|
|
# 验证可以获取到保存的值
|
|
|
|
|
get_response = client.get("/api/config")
|
|
|
|
|
get_data = get_response.json()
|
|
|
|
|
assert get_data["target_weight"] == 65.0
|
|
|
|
|
|
2026-01-22 12:57:26 +08:00
|
|
|
|
|
|
|
|
class TestTodayEndpoint:
|
|
|
|
|
"""今日概览接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_today(self, client, populated_db):
|
|
|
|
|
"""测试获取今日数据"""
|
|
|
|
|
response = client.get("/api/today")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["date"] == date.today().isoformat()
|
|
|
|
|
assert data["calories_intake"] > 0
|
|
|
|
|
assert data["exercise_count"] == 1
|
|
|
|
|
|
|
|
|
|
def test_today_has_meals(self, client, populated_db):
|
|
|
|
|
"""测试包含饮食"""
|
|
|
|
|
response = client.get("/api/today")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
assert len(data["meals"]) == 2
|
|
|
|
|
assert data["meals"][0]["meal_type"] in ["午餐", "早餐"]
|
|
|
|
|
|
|
|
|
|
def test_today_has_exercises(self, client, populated_db):
|
|
|
|
|
"""测试包含运动"""
|
|
|
|
|
response = client.get("/api/today")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
assert len(data["exercises"]) == 1
|
|
|
|
|
assert data["exercises"][0]["type"] == "跑步"
|
|
|
|
|
|
|
|
|
|
def test_today_empty(self, client):
|
|
|
|
|
"""测试空数据"""
|
|
|
|
|
response = client.get("/api/today")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["calories_intake"] == 0
|
|
|
|
|
assert data["exercise_count"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWeekEndpoint:
|
|
|
|
|
"""本周汇总接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_week(self, client, populated_db):
|
|
|
|
|
"""测试获取本周数据"""
|
|
|
|
|
response = client.get("/api/week")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "start_date" in data
|
|
|
|
|
assert "end_date" in data
|
|
|
|
|
assert "daily_stats" in data
|
|
|
|
|
|
|
|
|
|
def test_week_has_daily_stats(self, client, populated_db):
|
|
|
|
|
"""测试包含每日统计"""
|
|
|
|
|
response = client.get("/api/week")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
assert len(data["daily_stats"]) == 7
|
|
|
|
|
for stat in data["daily_stats"]:
|
|
|
|
|
assert "date" in stat
|
|
|
|
|
assert "weekday" in stat
|
|
|
|
|
|
|
|
|
|
def test_week_date_range(self, client, populated_db):
|
|
|
|
|
"""测试日期范围"""
|
|
|
|
|
response = client.get("/api/week")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
start = date.fromisoformat(data["start_date"])
|
|
|
|
|
end = date.fromisoformat(data["end_date"])
|
|
|
|
|
assert (end - start).days == 6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExercisesEndpoint:
|
|
|
|
|
"""运动记录接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_exercises(self, client, populated_db):
|
|
|
|
|
"""测试获取运动记录"""
|
|
|
|
|
response = client.get("/api/exercises")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 1
|
|
|
|
|
|
|
|
|
|
def test_exercises_with_days_param(self, client, populated_db):
|
|
|
|
|
"""测试天数参数"""
|
|
|
|
|
response = client.get("/api/exercises?days=7")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
def test_exercises_invalid_days(self, client):
|
|
|
|
|
"""测试无效天数"""
|
|
|
|
|
response = client.get("/api/exercises?days=0")
|
|
|
|
|
assert response.status_code == 422 # Validation error
|
|
|
|
|
|
|
|
|
|
def test_exercises_empty(self, client):
|
|
|
|
|
"""测试空数据"""
|
|
|
|
|
response = client.get("/api/exercises")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMealsEndpoint:
|
|
|
|
|
"""饮食记录接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_meals(self, client, populated_db):
|
|
|
|
|
"""测试获取饮食记录"""
|
|
|
|
|
response = client.get("/api/meals")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 2
|
|
|
|
|
|
|
|
|
|
def test_meals_structure(self, client, populated_db):
|
|
|
|
|
"""测试数据结构"""
|
|
|
|
|
response = client.get("/api/meals")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
meal = data[0]
|
|
|
|
|
assert "id" in meal
|
|
|
|
|
assert "date" in meal
|
|
|
|
|
assert "meal_type" in meal
|
|
|
|
|
assert "description" in meal
|
|
|
|
|
assert "calories" in meal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSleepEndpoint:
|
|
|
|
|
"""睡眠记录接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_sleep(self, client, populated_db):
|
|
|
|
|
"""测试获取睡眠记录"""
|
|
|
|
|
response = client.get("/api/sleep")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 1
|
|
|
|
|
|
|
|
|
|
def test_sleep_structure(self, client, populated_db):
|
|
|
|
|
"""测试数据结构"""
|
|
|
|
|
response = client.get("/api/sleep")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
record = data[0]
|
|
|
|
|
assert "duration" in record
|
|
|
|
|
assert "quality" in record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWeightEndpoint:
|
|
|
|
|
"""体重记录接口测试"""
|
|
|
|
|
|
|
|
|
|
def test_get_weight(self, client, populated_db):
|
|
|
|
|
"""测试获取体重记录"""
|
|
|
|
|
response = client.get("/api/weight")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data) >= 1
|
|
|
|
|
|
|
|
|
|
def test_weight_default_days(self, client, populated_db):
|
|
|
|
|
"""测试默认天数"""
|
|
|
|
|
response = client.get("/api/weight")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
|
|
def test_weight_structure(self, client, populated_db):
|
|
|
|
|
"""测试数据结构"""
|
|
|
|
|
response = client.get("/api/weight")
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
record = data[0]
|
|
|
|
|
assert "weight_kg" in record
|
|
|
|
|
assert record["weight_kg"] == 72.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCORS:
|
|
|
|
|
"""CORS 测试"""
|
|
|
|
|
|
|
|
|
|
def test_cors_headers(self, client):
|
|
|
|
|
"""测试 CORS 头"""
|
|
|
|
|
response = client.options("/api/config")
|
|
|
|
|
# FastAPI with CORSMiddleware should handle OPTIONS
|
|
|
|
|
assert response.status_code in [200, 405]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestErrorHandling:
|
|
|
|
|
"""错误处理测试"""
|
|
|
|
|
|
|
|
|
|
def test_not_found(self, client):
|
|
|
|
|
"""测试 404"""
|
|
|
|
|
response = client.get("/api/nonexistent")
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
def test_invalid_query_params(self, client):
|
|
|
|
|
"""测试无效参数"""
|
|
|
|
|
response = client.get("/api/exercises?days=abc")
|
|
|
|
|
assert response.status_code == 422
|
2026-01-27 16:16:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bmi_analysis_endpoint(client):
|
|
|
|
|
"""测试 BMI 分析 API"""
|
|
|
|
|
response = client.get("/api/bmi/analysis")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "current_weight" in data
|
|
|
|
|
assert "current_bmi" in data
|
|
|
|
|
assert "bmi_status" in data
|
|
|
|
|
assert "target_weight" in data
|
|
|
|
|
assert "estimated_days" in data
|
2026-01-27 16:19:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_nutrition_recommendations_endpoint(client):
|
|
|
|
|
"""测试营养建议 API"""
|
|
|
|
|
response = client.get("/api/nutrition/recommendations")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "daily_targets" in data
|
|
|
|
|
assert "today_intake" in data
|
|
|
|
|
assert "gaps" in data
|
|
|
|
|
assert "suggestions" in data
|
2026-01-27 16:22:55 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sleep_assessment_endpoint(client):
|
|
|
|
|
"""测试睡眠评估 API"""
|
|
|
|
|
response = client.get("/api/sleep/assessment")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "avg_duration" in data
|
|
|
|
|
assert "status" in data
|
|
|
|
|
assert "health_impacts" in data
|
|
|
|
|
assert "ideal_days_count" in data
|