import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from main import app from src.api.auth import get_db, FitbitCredentials from src.models import Configuration, APIToken @pytest.fixture def mock_db_session(): return MagicMock() @pytest.fixture def client(mock_db_session): def override_get_db(): try: yield mock_db_session finally: pass app.dependency_overrides[get_db] = override_get_db yield TestClient(app) app.dependency_overrides.clear() def test_save_fitbit_credentials(client, mock_db_session): """Test saving Fitbit credentials and generating auth URL.""" payload = { "client_id": "test_client_id", "client_secret": "test_client_secret", "redirect_uri": "http://localhost/callback" } # Mock DB query for existing config mock_db_session.query.return_value.first.return_value = None # Mock Config creation is handled by code logic (checks if exists, else creates) with patch("src.api.auth.FitbitClient") as MockFitbitClient: instance = MockFitbitClient.return_value instance.get_authorization_url.return_value = "https://www.fitbit.com/oauth2/authorize?client_id=test_client_id" response = client.post("/api/setup/fitbit", json=payload) assert response.status_code == 200 data = response.json() assert "auth_url" in data assert "test_client_id" in data["auth_url"] # Verify DB interactions # Should add new config assert mock_db_session.add.called assert mock_db_session.commit.called def test_fitbit_callback_success(client, mock_db_session): """Test Fitbit OAuth callback success.""" # Setup initial config in mock DB mock_config = MagicMock(spec=Configuration) mock_config.fitbit_client_id = "cid" mock_config.fitbit_client_secret = "csec" mock_config.fitbit_redirect_uri = "uri" mock_db_session.query.return_value.first.return_value = mock_config # Mock Token query (return None so it creates new) # query(Configuration).first() -> config # query(APIToken).filter_by().first() -> None (to trigger creation) def query_side_effect(model): m = MagicMock() if model == Configuration: m.first.return_value = mock_config elif model == APIToken: m.filter_by.return_value.first.return_value = None return m mock_db_session.query.side_effect = query_side_effect with patch("src.api.auth.FitbitClient") as MockFitbitClient: instance = MockFitbitClient.return_value instance.exchange_code_for_token.return_value = { "access_token": "new_at", "refresh_token": "new_rt", "expires_in": 3600, "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 assert mock_db_session.add.called # APIToken added assert mock_db_session.commit.called def test_fitbit_callback_no_config(client, mock_db_session): """Test callback fails if no config exists.""" # Mock DB returns None for config def query_side_effect(model): m = MagicMock() if model == Configuration: m.first.return_value = None # No config return m mock_db_session.query.side_effect = query_side_effect payload = {"code": "auth_code_123"} response = client.post("/api/setup/fitbit/callback", json=payload) assert response.status_code == 400 assert "Configuration missing" in response.json()["detail"]