import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from datetime import datetime, timedelta # Import models and app from src.models import Base, Configuration, APIToken from main import app from src.api.setup import get_db # Setup in-memory DB for tests SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @pytest.fixture(scope="module") def db_engine(): Base.metadata.create_all(bind=engine) yield engine Base.metadata.drop_all(bind=engine) @pytest.fixture(scope="function") def db(db_engine): connection = db_engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) yield session session.close() transaction.rollback() connection.close() @pytest.fixture(scope="function") def client(db): def override_get_db(): try: yield db finally: pass app.dependency_overrides[get_db] = override_get_db yield TestClient(app) del app.dependency_overrides[get_db] def test_save_fitbit_credentials(client, db): """Test saving Fitbit credentials and generating auth URL.""" payload = { "client_id": "test_client_id", "client_secret": "test_client_secret" } # Needs to match the Pydantic model we will create response = client.post("/api/setup/fitbit", json=payload) assert response.status_code == 200 data = response.json() assert "auth_url" in data assert "https://www.fitbit.com/oauth2/authorize" in data["auth_url"] assert "client_id=test_client_id" in data["auth_url"] # Verify DB config = db.query(Configuration).first() assert config is not None assert config.fitbit_client_id == "test_client_id" assert config.fitbit_client_secret == "test_client_secret" @patch("src.api.setup.FitbitClient") def test_fitbit_callback_success(mock_fitbit_cls, client, db): """Test Fitbit OAuth callback success.""" # Setup initial config config_entry = Configuration(fitbit_client_id="cid", fitbit_client_secret="csec") db.add(config_entry) db.commit() # Mock FitbitClient instance and method mock_instance = MagicMock() mock_fitbit_cls.return_value = mock_instance mock_instance.exchange_code_for_token.return_value = { "access_token": "new_at", "refresh_token": "new_rt", "expires_at": 3600, # seconds "user_id": "uid", "scope": ["weight"] } payload = {"code": "auth_code_123"} response = client.post("/api/setup/fitbit/callback", json=payload) assert response.status_code == 200 assert response.json()["status"] == "success" # Verify Token saved token = db.query(APIToken).filter_by(token_type="fitbit").first() assert token is not None assert token.access_token == "new_at" assert token.refresh_token == "new_rt" @patch("src.api.setup.FitbitClient") def test_fitbit_callback_no_config(mock_fitbit_cls, client, db): """Test callback fails if no config exists.""" payload = {"code": "auth_code_123"} response = client.post("/api/setup/fitbit/callback", json=payload) assert response.status_code == 400 assert "Configuration not found" in response.json()["detail"]