""" 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 uuid 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 from auth import sign_token # 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) @pytest.fixture(autouse=True) def _no_ai_jobs_background_thread(monkeypatch): """create_app() starts a daemon queue-consumer thread that outlives the test that launched it and races the *next* test for jobs on that test's fresh database (with whatever _runner the previous stub left behind) — which made queue tests flaky. Tests drive the queue themselves through jobs.run_once(), so the thread is disabled here. """ from services import jobs monkeypatch.setattr(jobs, "ENABLED", False) @pytest.fixture(autouse=True) def _clear_garmin_client_cache(): """Drop cached Garmin sessions between tests. `_connect` keeps an authenticated client per user for 15 minutes, so a stub installed by one test would otherwise be handed to the next one — and a test that expects `_connect` to be called would see it skipped. """ from services import garmin as garmin_svc garmin_svc._clients.clear() yield garmin_svc._clients.clear() @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() def _insert_user(email): """Create a user row directly, bypassing HTTP. Accounts now come from auth-hub's OAuth dance (see routes/auth.py); the test suite has no reason to exercise that network round trip just to get a user id and a valid JWT. """ uid = str(uuid.uuid4()) token = sign_token(uid) db_module.execute( "INSERT INTO users (id, email, auth_hub_username, jwt_token) VALUES (?, ?, ?, ?)", [uid, email, email, token], ) return {"id": uid, "email": email, "token": token} @pytest.fixture def make_user(db): """Factory for creating additional users, e.g. for cross-account isolation tests.""" return _insert_user @pytest.fixture def user(db): """A user, as if they had signed in through auth-hub: {id, email, token}.""" return _insert_user("tester@example.com") @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