From b87388721e0d818cbbf36dc5e4c8be3d9ac45c89 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 01:52:21 +0530 Subject: [PATCH 01/19] test(backend): add comprehensive endpoint integration tests for repos router --- tests/routers/test_repos.py | 251 ++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 tests/routers/test_repos.py diff --git a/tests/routers/test_repos.py b/tests/routers/test_repos.py new file mode 100644 index 0000000..018568c --- /dev/null +++ b/tests/routers/test_repos.py @@ -0,0 +1,251 @@ +import pytest +import sys +from unittest.mock import MagicMock +from fastapi.testclient import TestClient +from uuid import uuid4 + +# Windows local testing workaround: Mock 'rq' and 'redis' before app is imported +# This prevents the "cannot find context for 'fork'" error on Windows without modifying source code. +sys.modules['rq'] = MagicMock() +sys.modules['redis'] = MagicMock() + +from app.main import app +from app.deps import get_db_connection, get_current_user +from app.models.user import User +from app.models.repository import Repository +from app.models.drift import DriftEvent + +# Setup test client +client = TestClient(app) + +# Create a mock authenticated user +mock_user_id = uuid4() +mock_user = User( + id=mock_user_id, + email="tester@delta.com", + full_name="Jahnavi Tester", + github_user_id=123456789, + github_username="jahnavitest" +) + +# Global override for authentication dependency +def override_get_current_user(): + return mock_user + +app.dependency_overrides[get_current_user] = override_get_current_user + +# Fixture to provide a completely fresh mockup of the PostgreSQL database per test +@pytest.fixture +def mock_db_session(): + mock_db = MagicMock() + # Inject our mock db whenever fastapi asks for a database connection + app.dependency_overrides[get_db_connection] = lambda: mock_db + yield mock_db + pass + + +# =========== GET /repos/ Tests =========== + +def test_get_linked_repos_success(mock_db_session): + # Setup test data (2 repositories linked to the user) + repo1_id = uuid4() + repo2_id = uuid4() + + mock_repo_1 = Repository( + id=repo1_id, + installation_id=1, + repo_name="delta/backend", + is_active=True, + is_suspended=False, + style_preference="professional", + docs_root_path="/docs", + file_ignore_patterns=["/node_modules"] + ) + + mock_repo_2 = Repository( + id=repo2_id, + installation_id=1, + repo_name="delta/frontend", + is_active=False, + is_suspended=False, + style_preference="casual", + docs_root_path="/readme.md", + file_ignore_patterns=[] + ) + + # Tell the mock database to return our test data when query().join().filter().all() is called + mock_db_session.query.return_value.join.return_value.filter.return_value.all.return_value = [ + mock_repo_1, + mock_repo_2 + ] + + # Perform the API request using FastAPI TestClient + response = client.get("/api/repos/") + + # Assertions + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + assert data[0]["repo_name"] == "delta/backend" + assert data[1]["repo_name"] == "delta/frontend" + assert data[0]["style_preference"] == "professional" + + +# =========== PUT /repos/{id}/settings Tests =========== + +def test_update_repo_settings_success(mock_db_session): + repo_id = uuid4() + + # Initial repository state + mock_repo = Repository( + id=repo_id, + installation_id=2, + repo_name="delta/docs", + is_active=True, + is_suspended=False, + style_preference="professional", + docs_root_path="/docs", + file_ignore_patterns=[] + ) + + # Have the mock database return this repository when queried by ID + mock_db_session.query.return_value.join.return_value.filter.return_value.first.return_value = mock_repo + + # The payload simulating what the React frontend would send + update_payload = { + "style_preference": "casual", + "docs_root_path": "/new-docs-dir", + "file_ignore_patterns": ["/dist", "/build"] + } + + # Perform the API request + response = client.put(f"/api/repos/{repo_id}/settings", json=update_payload) + + # Assertions + assert response.status_code == 200 + data = response.json() + + # Verify the API response matches what we requested + assert data["style_preference"] == "casual" + assert data["docs_root_path"] == "/new-docs-dir" + assert data["file_ignore_patterns"] == ["/dist", "/build"] + + # Verify the database commit was called + mock_db_session.commit.assert_called_once() + mock_db_session.refresh.assert_called_once() + + +def test_update_repo_settings_not_found(mock_db_session): + repo_id = uuid4() + + # Simulate DB finding nothing + mock_db_session.query.return_value.join.return_value.filter.return_value.first.return_value = None + + update_payload = { + "style_preference": "casual" + } + + # Perform the API request + response = client.put(f"/api/repos/{repo_id}/settings", json=update_payload) + + # Ensure we get the correct 404 error + assert response.status_code == 404 + assert response.json()["detail"] == "Repository not found" + + # Ensure no commit was performed on failure + mock_db_session.commit.assert_not_called() + + +# =========== PATCH /repos/{id}/activate Tests =========== + +def test_toggle_repo_activation_success(mock_db_session): + repo_id = uuid4() + + mock_repo = Repository( + id=repo_id, + installation_id=3, + repo_name="delta/api", + is_active=False, + is_suspended=False + ) + + mock_db_session.query.return_value.join.return_value.filter.return_value.first.return_value = mock_repo + + activation_payload = {"is_active": True} + + response = client.patch(f"/api/repos/{repo_id}/activate", json=activation_payload) + + assert response.status_code == 200 + assert response.json()["is_active"] == True + mock_db_session.commit.assert_called_once() + + +def test_toggle_repo_activation_not_found(mock_db_session): + repo_id = uuid4() + mock_db_session.query.return_value.join.return_value.filter.return_value.first.return_value = None + + response = client.patch(f"/api/repos/{repo_id}/activate", json={"is_active": True}) + + assert response.status_code == 404 + mock_db_session.commit.assert_not_called() + + +# =========== GET /repos/{id}/drift-events Tests =========== + +def test_get_drift_events_success(mock_db_session): + repo_id = uuid4() + + # Mock finding the repository + mock_repo = Repository( + id=repo_id, + installation_id=1, + repo_name="delta/events", + is_active=True, + is_suspended=False + ) + + # Mock finding the drift events + event1_id = uuid4() + from datetime import datetime, UTC + mock_event = DriftEvent( + id=event1_id, + repo_id=repo_id, + pr_number=42, + base_branch="main", + head_branch="feature-branch", + base_sha="abc1234", + head_sha="def5678", + processing_phase="queued", + drift_result="pending", + created_at=datetime.now(UTC) + ) + + # Setup database mocks + # We first join+filter for the repo, then filter+order_by for events + mock_db_session.query.side_effect = [ + # First query: repo + MagicMock(join=MagicMock(return_value=MagicMock(filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=mock_repo)))))), + # Second query: events + MagicMock(filter=MagicMock(return_value=MagicMock(order_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[mock_event])))))) + ] + + response = client.get(f"/api/repos/{repo_id}/drift-events") + + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["processing_phase"] == "queued" + assert data[0]["pr_number"] == 42 + + +def test_get_drift_events_repo_not_found(mock_db_session): + repo_id = uuid4() + + # First query returns None (repo not found) + mock_db_session.query.side_effect = [ + MagicMock(join=MagicMock(return_value=MagicMock(filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=None)))))) + ] + + response = client.get(f"/api/repos/{repo_id}/drift-events") + + assert response.status_code == 404 From b8402534354ed2c348fc1d9d73cf62b037fee80a Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 01:55:34 +0530 Subject: [PATCH 02/19] fix(tests): resolve linting and naming conflicts --- tests/routers/{test_repos.py => test_api_repos.py} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename tests/routers/{test_repos.py => test_api_repos.py} (95%) diff --git a/tests/routers/test_repos.py b/tests/routers/test_api_repos.py similarity index 95% rename from tests/routers/test_repos.py rename to tests/routers/test_api_repos.py index 018568c..10baff4 100644 --- a/tests/routers/test_repos.py +++ b/tests/routers/test_api_repos.py @@ -9,11 +9,11 @@ sys.modules['rq'] = MagicMock() sys.modules['redis'] = MagicMock() -from app.main import app -from app.deps import get_db_connection, get_current_user -from app.models.user import User -from app.models.repository import Repository -from app.models.drift import DriftEvent +from app.main import app # noqa: E402 +from app.deps import get_db_connection, get_current_user # noqa: E402 +from app.models.user import User # noqa: E402 +from app.models.repository import Repository # noqa: E402 +from app.models.drift import DriftEvent # noqa: E402 # Setup test client client = TestClient(app) @@ -176,7 +176,7 @@ def test_toggle_repo_activation_success(mock_db_session): response = client.patch(f"/api/repos/{repo_id}/activate", json=activation_payload) assert response.status_code == 200 - assert response.json()["is_active"] == True + assert response.json()["is_active"] is True mock_db_session.commit.assert_called_once() From 70d69f139179fe126e3989b1440961c91bdf1773 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 01:57:48 +0530 Subject: [PATCH 03/19] fix(tests): remove global sys.modules mock that broke linux workers tests --- tests/routers/test_api_repos.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/routers/test_api_repos.py b/tests/routers/test_api_repos.py index 10baff4..dc40c6f 100644 --- a/tests/routers/test_api_repos.py +++ b/tests/routers/test_api_repos.py @@ -1,19 +1,13 @@ import pytest -import sys from unittest.mock import MagicMock from fastapi.testclient import TestClient from uuid import uuid4 -# Windows local testing workaround: Mock 'rq' and 'redis' before app is imported -# This prevents the "cannot find context for 'fork'" error on Windows without modifying source code. -sys.modules['rq'] = MagicMock() -sys.modules['redis'] = MagicMock() - -from app.main import app # noqa: E402 -from app.deps import get_db_connection, get_current_user # noqa: E402 -from app.models.user import User # noqa: E402 -from app.models.repository import Repository # noqa: E402 -from app.models.drift import DriftEvent # noqa: E402 +from app.main import app +from app.deps import get_db_connection, get_current_user +from app.models.user import User +from app.models.repository import Repository +from app.models.drift import DriftEvent # Setup test client client = TestClient(app) From d64a8b1e877333c029ea62f673bc1005c6ca244c Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 12:02:32 +0530 Subject: [PATCH 04/19] Add TestClient integration tests for auth and dashboard routes --- tests/routers/test_auth.py | 179 +++++++++++++++++++++++++ tests/routers/test_dashboard_routes.py | 143 ++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 tests/routers/test_auth.py create mode 100644 tests/routers/test_dashboard_routes.py diff --git a/tests/routers/test_auth.py b/tests/routers/test_auth.py new file mode 100644 index 0000000..f65b067 --- /dev/null +++ b/tests/routers/test_auth.py @@ -0,0 +1,179 @@ +import pytest +from unittest.mock import MagicMock, patch +from fastapi.testclient import TestClient +from uuid import uuid4 + +from app.main import app +from app.deps import get_db_connection, get_current_user +from app.models.user import User + +# =========== Setup =========== + +client = TestClient(app) + + +@pytest.fixture +def mock_db_session(): + """Provides a fresh mock database session and injects it as the FastAPI dependency.""" + mock_db = MagicMock() + app.dependency_overrides[get_db_connection] = lambda: mock_db + yield mock_db + app.dependency_overrides.pop(get_db_connection, None) + + +def make_mock_user(email="test@example.com", full_name="Test User"): + """Helper to create a mock User model instance.""" + user = MagicMock(spec=User) + user.id = uuid4() + user.email = email + user.full_name = full_name + user.password_hash = "hashed_password" + user.current_refresh_token_hash = None + return user + + +# =========== POST /auth/signup Tests =========== + + +def test_signup_success(mock_db_session): + """Test that a new user can register with valid credentials.""" + # No existing user found in DB + mock_db_session.query.return_value.filter.return_value.first.return_value = None + + mock_user = make_mock_user() + + with patch("app.routers.auth.security.get_hash", return_value="hashed_pw"), \ + patch("app.routers.auth.security.create_access_token", return_value="mock_access_token"), \ + patch("app.routers.auth.security.create_refresh_token", return_value="mock_refresh_token"), \ + patch("app.routers.auth.User") as MockUser: + + MockUser.return_value = mock_user + + response = client.post("/api/auth/signup", json={ + "email": "test@example.com", + "full_name": "Test User", + "password": "securepassword123" + }) + + assert response.status_code == 200 + data = response.json() + assert data["email"] == "test@example.com" + assert data["name"] == "Test User" + mock_db_session.add.assert_called_once() + mock_db_session.commit.assert_called() + + +def test_signup_duplicate_email(mock_db_session): + """Test that signup fails with 400 when email already exists.""" + existing_user = make_mock_user() + mock_db_session.query.return_value.filter.return_value.first.return_value = existing_user + + response = client.post("/api/auth/signup", json={ + "email": "test@example.com", + "full_name": "Test User", + "password": "securepassword123" + }) + + assert response.status_code == 400 + assert "already exists" in response.json()["detail"] + mock_db_session.add.assert_not_called() + + +def test_signup_invalid_email(mock_db_session): + """Test that signup fails with 422 when email format is invalid.""" + response = client.post("/api/auth/signup", json={ + "email": "not-a-valid-email", + "full_name": "Test User", + "password": "securepassword123" + }) + + assert response.status_code == 422 + + +# =========== POST /auth/login Tests =========== + + +def test_login_success(mock_db_session): + """Test that a user with correct credentials receives a successful response.""" + mock_user = make_mock_user() + mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user + + with patch("app.routers.auth.security.verify_hash", return_value=True), \ + patch("app.routers.auth.security.create_access_token", return_value="access_tok"), \ + patch("app.routers.auth.security.create_refresh_token", return_value="refresh_tok"), \ + patch("app.routers.auth.security.get_hash", return_value="hashed_refresh"): + + response = client.post("/api/auth/login", json={ + "email": "test@example.com", + "password": "correctpassword" + }) + + assert response.status_code == 200 + data = response.json() + assert data["email"] == "test@example.com" + assert data["name"] == "Test User" + mock_db_session.commit.assert_called() + + +def test_login_wrong_password(mock_db_session): + """Test that login fails with 401 when password is incorrect.""" + mock_user = make_mock_user() + mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user + + with patch("app.routers.auth.security.verify_hash", return_value=False): + response = client.post("/api/auth/login", json={ + "email": "test@example.com", + "password": "wrongpassword" + }) + + assert response.status_code == 401 + assert "Incorrect credentials" in response.json()["detail"] + + +def test_login_user_not_found(mock_db_session): + """Test that login fails with 401 when user does not exist.""" + mock_db_session.query.return_value.filter.return_value.first.return_value = None + + response = client.post("/api/auth/login", json={ + "email": "nobody@example.com", + "password": "anypassword" + }) + + assert response.status_code == 401 + + +def test_login_missing_fields(mock_db_session): + """Test that login fails with 422 when required fields are missing.""" + response = client.post("/api/auth/login", json={ + "email": "test@example.com" + # missing password + }) + + assert response.status_code == 422 + + +# =========== POST /auth/logout Tests =========== + + +def test_logout_with_valid_token(mock_db_session): + """Test that logout clears cookies and nullifies the refresh token hash.""" + mock_user = make_mock_user() + mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user + + with patch("app.routers.auth.security.verify_token", return_value={"sub": str(mock_user.id)}): + response = client.post( + "/api/auth/logout", + cookies={"access_token": "valid_access_token"} + ) + + assert response.status_code == 200 + assert response.json()["message"] == "Logout successful" + mock_db_session.commit.assert_called() + + +def test_logout_without_token(mock_db_session): + """Test that logout succeeds gracefully even with no cookies.""" + response = client.post("/api/auth/logout") + + assert response.status_code == 200 + assert response.json()["message"] == "Logout successful" diff --git a/tests/routers/test_dashboard_routes.py b/tests/routers/test_dashboard_routes.py new file mode 100644 index 0000000..16a103b --- /dev/null +++ b/tests/routers/test_dashboard_routes.py @@ -0,0 +1,143 @@ +import pytest +from unittest.mock import MagicMock, AsyncMock, patch +from fastapi.testclient import TestClient +from uuid import uuid4 + +from app.main import app +from app.deps import get_db_connection, get_current_user +from app.models.user import User + +# =========== Setup =========== + +client = TestClient(app) + +# Create a mock authenticated user for all dashboard tests +mock_user_id = uuid4() +mock_user = MagicMock(spec=User) +mock_user.id = mock_user_id +mock_user.email = "tester@delta.com" +mock_user.full_name = "Jahnavi Tester" + + +def override_get_current_user(): + return mock_user + + +app.dependency_overrides[get_current_user] = override_get_current_user + + +@pytest.fixture +def mock_db_session(): + """Provides a fresh mock database session injected as a FastAPI dependency.""" + mock_db = MagicMock() + app.dependency_overrides[get_db_connection] = lambda: mock_db + yield mock_db + app.dependency_overrides.pop(get_db_connection, None) + + +# =========== GET /dashboard/stats Tests =========== + + +def test_get_dashboard_stats_success(mock_db_session): + """Test that dashboard stats returns correct counts for the authenticated user.""" + mock_db_session.query.return_value.filter.return_value.scalar.return_value = 3 + mock_db_session.query.return_value.join.return_value.filter.return_value.scalar.return_value = 7 + mock_db_session.query.return_value.join.return_value.join.return_value.filter.return_value.scalar.return_value = 21 + + response = client.get("/api/dashboard/stats") + + assert response.status_code == 200 + data = response.json() + assert "installations_count" in data + assert "repos_linked_count" in data + assert "drift_events_count" in data + assert "pr_waiting_count" in data + + +def test_get_dashboard_stats_all_zero(mock_db_session): + """Test that dashboard stats returns zeros for a new user with no data.""" + mock_db_session.query.return_value.filter.return_value.scalar.return_value = 0 + mock_db_session.query.return_value.join.return_value.filter.return_value.scalar.return_value = 0 + mock_db_session.query.return_value.join.return_value.join.return_value.filter.return_value.scalar.return_value = 0 + + response = client.get("/api/dashboard/stats") + + assert response.status_code == 200 + data = response.json() + assert data["installations_count"] == 0 + assert data["repos_linked_count"] == 0 + assert data["drift_events_count"] == 0 + assert data["pr_waiting_count"] == 0 + + +def test_get_dashboard_stats_requires_auth(): + """Test that dashboard stats requires authentication.""" + app.dependency_overrides.pop(get_current_user, None) + + response = client.get("/api/dashboard/stats") + + # Restore override + app.dependency_overrides[get_current_user] = override_get_current_user + + assert response.status_code in [401, 403] + + +# =========== GET /dashboard/repos Tests =========== + + +def test_get_dashboard_repos_success(mock_db_session): + """Test that dashboard repos returns repo details for the authenticated user.""" + mock_repo = MagicMock() + mock_repo.repo_name = "owner/delta-docs" + mock_repo.installation_id = 101 + + mock_db_session.query.return_value.join.return_value.filter.return_value \ + .order_by.return_value.limit.return_value.all.return_value = [mock_repo] + + with patch("app.routers.dashboard.get_repo_details", new_callable=AsyncMock) as mock_details: + mock_details.return_value = { + "name": "delta-docs", + "description": "Documentation drift detector", + "language": "Python", + "stargazers_count": 42, + "forks_count": 5, + "avatar_url": None, + } + response = client.get("/api/dashboard/repos") + + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["name"] == "delta-docs" + assert data[0]["language"] == "Python" + assert data[0]["stargazers_count"] == 42 + + +def test_get_dashboard_repos_empty(mock_db_session): + """Test that dashboard repos returns empty list when user has no repos.""" + mock_db_session.query.return_value.join.return_value.filter.return_value \ + .order_by.return_value.limit.return_value.all.return_value = [] + + response = client.get("/api/dashboard/repos") + + assert response.status_code == 200 + assert response.json() == [] + + +def test_get_dashboard_repos_github_api_failure(mock_db_session): + """Test that dashboard repos falls back gracefully when GitHub API fails.""" + mock_repo = MagicMock() + mock_repo.repo_name = "owner/delta-docs" + mock_repo.installation_id = 101 + + mock_db_session.query.return_value.join.return_value.filter.return_value \ + .order_by.return_value.limit.return_value.all.return_value = [mock_repo] + + with patch("app.routers.dashboard.get_repo_details", side_effect=Exception("GitHub API down")): + response = client.get("/api/dashboard/repos") + + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["description"] == "Error fetching details" + assert data[0]["name"] == "owner/delta-docs" From 62e6398d5fbf5e6421893fe54c36419dc847c1ca Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 12:06:29 +0530 Subject: [PATCH 05/19] Fix CI: rename test_auth.py to test_auth_routes.py to avoid module name collision, remove unused import --- tests/routers/{test_auth.py => test_auth_routes.py} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename tests/routers/{test_auth.py => test_auth_routes.py} (98%) diff --git a/tests/routers/test_auth.py b/tests/routers/test_auth_routes.py similarity index 98% rename from tests/routers/test_auth.py rename to tests/routers/test_auth_routes.py index f65b067..ba78537 100644 --- a/tests/routers/test_auth.py +++ b/tests/routers/test_auth_routes.py @@ -4,7 +4,7 @@ from uuid import uuid4 from app.main import app -from app.deps import get_db_connection, get_current_user +from app.deps import get_db_connection from app.models.user import User # =========== Setup =========== @@ -37,7 +37,6 @@ def make_mock_user(email="test@example.com", full_name="Test User"): def test_signup_success(mock_db_session): """Test that a new user can register with valid credentials.""" - # No existing user found in DB mock_db_session.query.return_value.filter.return_value.first.return_value = None mock_user = make_mock_user() From 0be86554d55cb2f68c7f0e2690284c64c46ce17c Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 12:30:48 +0530 Subject: [PATCH 06/19] Add TestClient integration tests for notifications routes --- tests/routers/test_notification_routes.py | 161 ++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/routers/test_notification_routes.py diff --git a/tests/routers/test_notification_routes.py b/tests/routers/test_notification_routes.py new file mode 100644 index 0000000..c1d2d61 --- /dev/null +++ b/tests/routers/test_notification_routes.py @@ -0,0 +1,161 @@ +import pytest +from unittest.mock import MagicMock +from fastapi.testclient import TestClient +from uuid import uuid4 + +from app.main import app +from app.deps import get_db_connection, get_current_user +from app.models.user import User +from app.models.notification import Notification + +# =========== Setup =========== + +client = TestClient(app) + +mock_user_id = uuid4() +mock_user = MagicMock(spec=User) +mock_user.id = mock_user_id +mock_user.email = "tester@delta.com" +mock_user.full_name = "Jahnavi Tester" + + +def override_get_current_user(): + return mock_user + + +app.dependency_overrides[get_current_user] = override_get_current_user + + +@pytest.fixture +def mock_db_session(): + """Provides a fresh mock database session injected as a FastAPI dependency.""" + mock_db = MagicMock() + app.dependency_overrides[get_db_connection] = lambda: mock_db + yield mock_db + app.dependency_overrides.pop(get_db_connection, None) + + +def make_mock_notification(is_read=False): + """Helper to create a mock Notification instance.""" + notif = MagicMock(spec=Notification) + notif.id = uuid4() + notif.user_id = mock_user_id + notif.message = "New drift event detected in delta/backend" + notif.is_read = is_read + notif.created_at = "2026-03-11T06:00:00Z" + return notif + + +# =========== GET /notifications/ Tests =========== + + +def test_get_notifications_success(mock_db_session): + """Test that a user can retrieve their notifications in descending order.""" + notif1 = make_mock_notification() + notif2 = make_mock_notification(is_read=True) + + mock_db_session.query.return_value.filter.return_value \ + .order_by.return_value.all.return_value = [notif1, notif2] + + response = client.get("/api/notifications/") + + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + + +def test_get_notifications_empty(mock_db_session): + """Test that an empty list is returned when user has no notifications.""" + mock_db_session.query.return_value.filter.return_value \ + .order_by.return_value.all.return_value = [] + + response = client.get("/api/notifications/") + + assert response.status_code == 200 + assert response.json() == [] + + +# =========== PATCH /notifications/{id}/read Tests =========== + + +def test_mark_notification_as_read_success(mock_db_session): + """Test that a notification can be marked as read.""" + notif = make_mock_notification(is_read=False) + notification_id = notif.id + + mock_db_session.query.return_value.filter.return_value.first.return_value = notif + + response = client.patch(f"/api/notifications/{notification_id}/read") + + assert response.status_code == 200 + assert notif.is_read is True + mock_db_session.commit.assert_called_once() + mock_db_session.refresh.assert_called_once_with(notif) + + +def test_mark_notification_as_read_not_found(mock_db_session): + """Test that marking a non-existent notification raises 404.""" + mock_db_session.query.return_value.filter.return_value.first.return_value = None + + response = client.patch(f"/api/notifications/{uuid4()}/read") + + assert response.status_code == 404 + assert "Notification not found" in response.json()["detail"] + mock_db_session.commit.assert_not_called() + + +# =========== PATCH /notifications/read-all Tests =========== + + +def test_mark_all_notifications_as_read(mock_db_session): + """Test that all unread notifications can be marked as read at once.""" + mock_db_session.query.return_value.filter.return_value.update.return_value = 3 + + response = client.patch("/api/notifications/read-all") + + assert response.status_code == 200 + assert response.json()["message"] == "All notifications marked as read" + mock_db_session.commit.assert_called_once() + + +# =========== DELETE /notifications/{id} Tests =========== + + +def test_delete_notification_success(mock_db_session): + """Test that a notification can be deleted.""" + notif = make_mock_notification() + notification_id = notif.id + + mock_db_session.query.return_value.filter.return_value.first.return_value = notif + + response = client.delete(f"/api/notifications/{notification_id}") + + assert response.status_code == 200 + assert response.json()["message"] == "Notification deleted" + mock_db_session.delete.assert_called_once_with(notif) + mock_db_session.commit.assert_called_once() + + +def test_delete_notification_not_found(mock_db_session): + """Test that deleting a non-existent notification raises 404.""" + mock_db_session.query.return_value.filter.return_value.first.return_value = None + + response = client.delete(f"/api/notifications/{uuid4()}") + + assert response.status_code == 404 + assert "Notification not found" in response.json()["detail"] + mock_db_session.delete.assert_not_called() + + +# =========== DELETE /notifications/ Tests =========== + + +def test_delete_all_notifications(mock_db_session): + """Test that all notifications for the user can be deleted at once.""" + mock_db_session.query.return_value.filter.return_value.delete.return_value = 5 + + response = client.delete("/api/notifications/") + + assert response.status_code == 200 + assert response.json()["message"] == "All notifications deleted" + mock_db_session.commit.assert_called_once() From 0793c947c320da13026b02eaa64fb34cfe84be9c Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 12:34:19 +0530 Subject: [PATCH 07/19] Fix notification tests: use real Notification instances for proper schema serialization --- tests/routers/test_notification_routes.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/routers/test_notification_routes.py b/tests/routers/test_notification_routes.py index c1d2d61..a51c38a 100644 --- a/tests/routers/test_notification_routes.py +++ b/tests/routers/test_notification_routes.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock from fastapi.testclient import TestClient from uuid import uuid4 +from datetime import datetime, timezone from app.main import app from app.deps import get_db_connection, get_current_user @@ -36,13 +37,14 @@ def mock_db_session(): def make_mock_notification(is_read=False): - """Helper to create a mock Notification instance.""" - notif = MagicMock(spec=Notification) - notif.id = uuid4() - notif.user_id = mock_user_id - notif.message = "New drift event detected in delta/backend" - notif.is_read = is_read - notif.created_at = "2026-03-11T06:00:00Z" + """Helper to create a real Notification model instance with proper typed fields.""" + notif = Notification( + id=uuid4(), + user_id=mock_user_id, + content="New drift event detected in delta/backend", + is_read=is_read, + created_at=datetime.now(timezone.utc), + ) return notif From 55439cd11140a0cfbccb9eae85308c689f965d03 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 12:58:15 +0530 Subject: [PATCH 08/19] Rename test_api_repos.py to test_repos_routes.py for consistent naming --- tests/routers/{test_api_repos.py => test_repos_routes.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/routers/{test_api_repos.py => test_repos_routes.py} (100%) diff --git a/tests/routers/test_api_repos.py b/tests/routers/test_repos_routes.py similarity index 100% rename from tests/routers/test_api_repos.py rename to tests/routers/test_repos_routes.py From 8c97f2c978a3fb08205c60c3104de7c583b7c978 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 13:02:36 +0530 Subject: [PATCH 09/19] Add missing drift event detail endpoint integration tests (GET drift-events/{event_id}) --- tests/routers/test_repos_routes.py | 123 +++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/routers/test_repos_routes.py b/tests/routers/test_repos_routes.py index dc40c6f..238bda7 100644 --- a/tests/routers/test_repos_routes.py +++ b/tests/routers/test_repos_routes.py @@ -243,3 +243,126 @@ def test_get_drift_events_repo_not_found(mock_db_session): response = client.get(f"/api/repos/{repo_id}/drift-events") assert response.status_code == 404 + + +# =========== GET /repos/{id}/drift-events/{event_id} Tests =========== + + +def test_get_drift_event_detail_success(mock_db_session): + """Test that a single drift event with its findings and code changes is returned.""" + from datetime import datetime, UTC + from app.models.drift import DriftFinding, CodeChange + + repo_id = uuid4() + event_id = uuid4() + + mock_repo = Repository( + id=repo_id, + installation_id=1, + repo_name="delta/events", + is_active=True, + is_suspended=False, + docs_root_path="/docs" + ) + + mock_event = DriftEvent( + id=event_id, + repo_id=repo_id, + pr_number=99, + base_branch="main", + head_branch="feature-ai", + base_sha="aaa111", + head_sha="bbb222", + processing_phase="completed", + drift_result="drift_found", + created_at=datetime.now(UTC) + ) + + mock_finding = DriftFinding( + id=uuid4(), + drift_event_id=event_id, + code_path="src/agent.py", + doc_file_path="/docs/agent.md", + change_type="modified", + drift_type="outdated_description", + drift_score=0.85, + explanation="Function signature changed", + confidence=0.9, + created_at=datetime.now(UTC) + ) + + mock_code_change = CodeChange( + id=uuid4(), + drift_event_id=event_id, + file_path="src/agent.py", + change_type="modified", + is_code=True, + is_ignored=False + ) + + # 4 sequential DB queries: repo → event → findings → code changes + mock_db_session.query.side_effect = [ + MagicMock(join=MagicMock(return_value=MagicMock( + filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=mock_repo))) + ))), + MagicMock(filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=mock_event)))), + MagicMock(filter=MagicMock(return_value=MagicMock( + order_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[mock_finding]))) + ))), + MagicMock(filter=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[mock_code_change])))), + ] + + response = client.get(f"/api/repos/{repo_id}/drift-events/{event_id}") + + assert response.status_code == 200 + data = response.json() + assert data["pr_number"] == 99 + assert data["drift_result"] == "drift_found" + assert data["processing_phase"] == "completed" + assert len(data["findings"]) == 1 + assert data["findings"][0]["code_path"] == "src/agent.py" + assert data["findings"][0]["drift_score"] == 0.85 + assert len(data["code_changes"]) == 1 + assert data["code_changes"][0]["file_path"] == "src/agent.py" + + +def test_get_drift_event_detail_repo_not_found(mock_db_session): + """Test that 404 is returned when repo does not belong to the user.""" + mock_db_session.query.side_effect = [ + MagicMock(join=MagicMock(return_value=MagicMock( + filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=None))) + ))), + ] + + response = client.get(f"/api/repos/{uuid4()}/drift-events/{uuid4()}") + + assert response.status_code == 404 + assert response.json()["detail"] == "Repository not found" + + +def test_get_drift_event_detail_event_not_found(mock_db_session): + """Test that 404 is returned when the drift event ID does not exist.""" + from datetime import datetime, UTC + + repo_id = uuid4() + mock_repo = Repository( + id=repo_id, + installation_id=1, + repo_name="delta/events", + is_active=True, + is_suspended=False, + docs_root_path="/docs" + ) + + mock_db_session.query.side_effect = [ + MagicMock(join=MagicMock(return_value=MagicMock( + filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=mock_repo))) + ))), + MagicMock(filter=MagicMock(return_value=MagicMock(first=MagicMock(return_value=None)))), + ] + + response = client.get(f"/api/repos/{repo_id}/drift-events/{uuid4()}") + + assert response.status_code == 404 + assert response.json()["detail"] == "Drift event not found" + From fae0c0f05c326a0c143783c87ced634cb0293ea1 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 13:22:18 +0530 Subject: [PATCH 10/19] fix: add missing optional fields to mock_event and remove unused imports in test_repos_routes --- tests/routers/test_repos_routes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/routers/test_repos_routes.py b/tests/routers/test_repos_routes.py index 238bda7..bf8e9cb 100644 --- a/tests/routers/test_repos_routes.py +++ b/tests/routers/test_repos_routes.py @@ -275,7 +275,11 @@ def test_get_drift_event_detail_success(mock_db_session): head_sha="bbb222", processing_phase="completed", drift_result="drift_found", - created_at=datetime.now(UTC) + created_at=datetime.now(UTC), + overall_drift_score=None, + error_message=None, + started_at=None, + completed_at=None, ) mock_finding = DriftFinding( @@ -342,8 +346,6 @@ def test_get_drift_event_detail_repo_not_found(mock_db_session): def test_get_drift_event_detail_event_not_found(mock_db_session): """Test that 404 is returned when the drift event ID does not exist.""" - from datetime import datetime, UTC - repo_id = uuid4() mock_repo = Repository( id=repo_id, From 037550b19189f9d2ecf653a3013391a425c515d6 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 13:33:16 +0530 Subject: [PATCH 11/19] fix(auth-test): add github oauth callback tests and fix deprecation warning --- tests/routers/test_auth_routes.py | 67 +++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index ba78537..ddebb42 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -21,6 +21,16 @@ def mock_db_session(): app.dependency_overrides.pop(get_db_connection, None) +@pytest.fixture +def mock_current_user(): + """Fixture to mock the currently authenticated user for dependency injection.""" + from app.deps import get_current_user + user = make_mock_user() + app.dependency_overrides[get_current_user] = lambda: user + yield user + app.dependency_overrides.pop(get_current_user, None) + + def make_mock_user(email="test@example.com", full_name="Test User"): """Helper to create a mock User model instance.""" user = MagicMock(spec=User) @@ -160,10 +170,9 @@ def test_logout_with_valid_token(mock_db_session): mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user with patch("app.routers.auth.security.verify_token", return_value={"sub": str(mock_user.id)}): - response = client.post( - "/api/auth/logout", - cookies={"access_token": "valid_access_token"} - ) + client.cookies.set("access_token", "valid_access_token") + response = client.post("/api/auth/logout") + client.cookies.delete("access_token") assert response.status_code == 200 assert response.json()["message"] == "Logout successful" @@ -176,3 +185,53 @@ def test_logout_without_token(mock_db_session): assert response.status_code == 200 assert response.json()["message"] == "Logout successful" + + +# =========== GET /auth/github/callback Tests =========== + +@pytest.mark.asyncio +async def test_github_callback_success(mock_db_session, mock_current_user): + """Test successful GitHub OAuth callback linking user and installation.""" + from app.models.installation import Installation + + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = {"access_token": "gh_access_token"} + + mock_user_resp = MagicMock() + mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} + + with patch("httpx.AsyncClient.post", return_value=mock_token_resp), \ + patch("httpx.AsyncClient.get", return_value=mock_user_resp): + + response = client.get("/api/auth/github/callback?code=mock_code&installation_id=67890") + + assert response.status_code == 303 + assert "dashboard" in response.headers["location"] + + assert mock_current_user.github_user_id == 12345 + assert mock_current_user.github_username == "github_user" + mock_db_session.commit.assert_called() + + +@pytest.mark.asyncio +async def test_github_callback_missing_code(mock_db_session, mock_current_user): + """Test that missing authorization code returns 400.""" + response = client.get("/api/auth/github/callback?installation_id=67890") + assert response.status_code == 400 + assert "code missing" in response.json()["detail"].lower() + + +@pytest.mark.asyncio +async def test_github_callback_github_error(mock_db_session, mock_current_user): + """Test handling of error returned by GitHub during token exchange.""" + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = { + "error": "bad_verification_code", + "error_description": "The code passed is incorrect or expired." + } + + with patch("httpx.AsyncClient.post", return_value=mock_token_resp): + response = client.get("/api/auth/github/callback?code=wrong_code") + + assert response.status_code == 400 + assert "GitHub Error" in response.json()["detail"] From 17c6b4556d824c665b3d4fe7500ecb85bf942552 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 13:37:22 +0530 Subject: [PATCH 12/19] fix(auth-test): refactor to sync, fix dependency leak and use AsyncMock --- tests/routers/test_auth_routes.py | 40 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index ddebb42..56eb880 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, AsyncMock, patch from fastapi.testclient import TestClient from uuid import uuid4 @@ -12,23 +12,30 @@ client = TestClient(app) +@pytest.fixture(autouse=True) +def setup_auth_overrides(): + """Sets up default authentication overrides for all tests in this file.""" + app.dependency_overrides[get_current_user] = lambda: mock_user + yield + # We keep it to avoid breaking other files + pass + + @pytest.fixture def mock_db_session(): """Provides a fresh mock database session and injects it as the FastAPI dependency.""" mock_db = MagicMock() app.dependency_overrides[get_db_connection] = lambda: mock_db yield mock_db - app.dependency_overrides.pop(get_db_connection, None) + # We don't pop to avoid breaking other files relying on side effects + pass @pytest.fixture def mock_current_user(): - """Fixture to mock the currently authenticated user for dependency injection.""" - from app.deps import get_current_user - user = make_mock_user() - app.dependency_overrides[get_current_user] = lambda: user - yield user - app.dependency_overrides.pop(get_current_user, None) + """Fixture to provide the mock user and ensure it's set in overrides.""" + app.dependency_overrides[get_current_user] = lambda: mock_user + return mock_user def make_mock_user(email="test@example.com", full_name="Test User"): @@ -189,15 +196,12 @@ def test_logout_without_token(mock_db_session): # =========== GET /auth/github/callback Tests =========== -@pytest.mark.asyncio -async def test_github_callback_success(mock_db_session, mock_current_user): +def test_github_callback_success(mock_db_session, mock_current_user): """Test successful GitHub OAuth callback linking user and installation.""" - from app.models.installation import Installation - - mock_token_resp = MagicMock() + mock_token_resp = AsyncMock() mock_token_resp.json.return_value = {"access_token": "gh_access_token"} - mock_user_resp = MagicMock() + mock_user_resp = AsyncMock() mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} with patch("httpx.AsyncClient.post", return_value=mock_token_resp), \ @@ -213,18 +217,16 @@ async def test_github_callback_success(mock_db_session, mock_current_user): mock_db_session.commit.assert_called() -@pytest.mark.asyncio -async def test_github_callback_missing_code(mock_db_session, mock_current_user): +def test_github_callback_missing_code(mock_db_session, mock_current_user): """Test that missing authorization code returns 400.""" response = client.get("/api/auth/github/callback?installation_id=67890") assert response.status_code == 400 assert "code missing" in response.json()["detail"].lower() -@pytest.mark.asyncio -async def test_github_callback_github_error(mock_db_session, mock_current_user): +def test_github_callback_github_error(mock_db_session, mock_current_user): """Test handling of error returned by GitHub during token exchange.""" - mock_token_resp = MagicMock() + mock_token_resp = AsyncMock() mock_token_resp.json.return_value = { "error": "bad_verification_code", "error_description": "The code passed is incorrect or expired." From fe7f9ab7d205f93b7d93b40dba2be130ab2743ab Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 13:40:23 +0530 Subject: [PATCH 13/19] fix(auth-test): restore missing get_current_user import and mock_user global --- tests/routers/test_auth_routes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 56eb880..7874321 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -4,7 +4,7 @@ from uuid import uuid4 from app.main import app -from app.deps import get_db_connection +from app.deps import get_db_connection, get_current_user from app.models.user import User # =========== Setup =========== @@ -49,6 +49,10 @@ def make_mock_user(email="test@example.com", full_name="Test User"): return user +# Create a mock authenticated user for global use in this file +mock_user = make_mock_user() + + # =========== POST /auth/signup Tests =========== From cf9905ae6de084407b178dc5b459e5cd566a7aa2 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:14:11 +0530 Subject: [PATCH 14/19] fix(auth-test): localized httpx patch and safer overrides --- tests/routers/test_auth_routes.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 7874321..8be89ea 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -14,21 +14,23 @@ @pytest.fixture(autouse=True) def setup_auth_overrides(): - """Sets up default authentication overrides for all tests in this file.""" + """Sets up authentication overrides and resets them after each test.""" + # Store original overrides to restore them later + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[get_current_user] = lambda: mock_user + app.dependency_overrides[get_db_connection] = lambda: MagicMock() + yield - # We keep it to avoid breaking other files - pass + + # Restore original overrides + app.dependency_overrides = original_overrides @pytest.fixture def mock_db_session(): - """Provides a fresh mock database session and injects it as the FastAPI dependency.""" - mock_db = MagicMock() - app.dependency_overrides[get_db_connection] = lambda: mock_db - yield mock_db - # We don't pop to avoid breaking other files relying on side effects - pass + """Provides the active mock database session.""" + return app.dependency_overrides[get_db_connection]() @pytest.fixture @@ -208,8 +210,9 @@ def test_github_callback_success(mock_db_session, mock_current_user): mock_user_resp = AsyncMock() mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - with patch("httpx.AsyncClient.post", return_value=mock_token_resp), \ - patch("httpx.AsyncClient.get", return_value=mock_user_resp): + # Patch ONLY inside the router module to avoid breaking TestClient + with patch("app.routers.auth.httpx.AsyncClient.post", return_value=mock_token_resp), \ + patch("app.routers.auth.httpx.AsyncClient.get", return_value=mock_user_resp): response = client.get("/api/auth/github/callback?code=mock_code&installation_id=67890") @@ -236,7 +239,7 @@ def test_github_callback_github_error(mock_db_session, mock_current_user): "error_description": "The code passed is incorrect or expired." } - with patch("httpx.AsyncClient.post", return_value=mock_token_resp): + with patch("app.routers.auth.httpx.AsyncClient.post", return_value=mock_token_resp): response = client.get("/api/auth/github/callback?code=wrong_code") assert response.status_code == 400 From 1d501eae913893f7b0faab7ccdd4f25e9ae4b9b6 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:20:25 +0530 Subject: [PATCH 15/19] fix(auth-test): robust fixtures, localized httpx patching, and valid mock attributes --- tests/routers/test_auth_routes.py | 81 +++++++++++++++++-------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 8be89ea..10be02f 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -3,6 +3,7 @@ from fastapi.testclient import TestClient from uuid import uuid4 +from sqlalchemy.orm import Session from app.main import app from app.deps import get_db_connection, get_current_user from app.models.user import User @@ -13,45 +14,45 @@ @pytest.fixture(autouse=True) -def setup_auth_overrides(): - """Sets up authentication overrides and resets them after each test.""" - # Store original overrides to restore them later - original_overrides = app.dependency_overrides.copy() - - app.dependency_overrides[get_current_user] = lambda: mock_user - app.dependency_overrides[get_db_connection] = lambda: MagicMock() - +def setup_overrides(): + """Preserves and restores dependency_overrides after each test.""" + old_overrides = app.dependency_overrides.copy() yield - - # Restore original overrides - app.dependency_overrides = original_overrides + app.dependency_overrides = old_overrides @pytest.fixture def mock_db_session(): - """Provides the active mock database session.""" - return app.dependency_overrides[get_db_connection]() + """Provides a fresh mock database session and sets it in overrides.""" + mock_db = MagicMock(spec=Session) + # Default behavior for query chain + mock_db.query.return_value.filter.return_value.first.return_value = None + app.dependency_overrides[get_db_connection] = lambda: mock_db + return mock_db @pytest.fixture def mock_current_user(): - """Fixture to provide the mock user and ensure it's set in overrides.""" - app.dependency_overrides[get_current_user] = lambda: mock_user - return mock_user + """Fixture to provide a mock user and set it in overrides.""" + user = make_mock_user() + app.dependency_overrides[get_current_user] = lambda: user + return user def make_mock_user(email="test@example.com", full_name="Test User"): - """Helper to create a mock User model instance.""" + """Helper to create a mock User model instance with valid string attributes.""" user = MagicMock(spec=User) user.id = uuid4() - user.email = email - user.full_name = full_name - user.password_hash = "hashed_password" + user.email = str(email) + user.full_name = str(full_name) + user.password_hash = "hashed_pw" user.current_refresh_token_hash = None + user.github_user_id = None + user.github_username = None return user -# Create a mock authenticated user for global use in this file +# Create a default mock user for general use mock_user = make_mock_user() @@ -204,15 +205,18 @@ def test_logout_without_token(mock_db_session): def test_github_callback_success(mock_db_session, mock_current_user): """Test successful GitHub OAuth callback linking user and installation.""" - mock_token_resp = AsyncMock() - mock_token_resp.json.return_value = {"access_token": "gh_access_token"} - - mock_user_resp = AsyncMock() - mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - - # Patch ONLY inside the router module to avoid breaking TestClient - with patch("app.routers.auth.httpx.AsyncClient.post", return_value=mock_token_resp), \ - patch("app.routers.auth.httpx.AsyncClient.get", return_value=mock_user_resp): + # Patch the httpx module reference in the router to avoid breaking TestClient + with patch("app.routers.auth.httpx") as mock_httpx: + # Configure AsyncMock for the post/get calls + mock_client = mock_httpx.AsyncClient.return_value.__aenter__.return_value + + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = {"access_token": "gh_access_token"} + mock_client.post.return_value = mock_token_resp + + mock_user_resp = MagicMock() + mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} + mock_client.get.return_value = mock_user_resp response = client.get("/api/auth/github/callback?code=mock_code&installation_id=67890") @@ -233,13 +237,16 @@ def test_github_callback_missing_code(mock_db_session, mock_current_user): def test_github_callback_github_error(mock_db_session, mock_current_user): """Test handling of error returned by GitHub during token exchange.""" - mock_token_resp = AsyncMock() - mock_token_resp.json.return_value = { - "error": "bad_verification_code", - "error_description": "The code passed is incorrect or expired." - } - - with patch("app.routers.auth.httpx.AsyncClient.post", return_value=mock_token_resp): + with patch("app.routers.auth.httpx") as mock_httpx: + mock_client = mock_httpx.AsyncClient.return_value.__aenter__.return_value + + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = { + "error": "bad_verification_code", + "error_description": "The code passed is incorrect or expired." + } + mock_client.post.return_value = mock_token_resp + response = client.get("/api/auth/github/callback?code=wrong_code") assert response.status_code == 400 From d6f0685935222427f67fd83780b807f199d3c6f8 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:23:32 +0530 Subject: [PATCH 16/19] fix(auth-test): resolve 405 and lint issues with patch.dict and clean mocks --- tests/routers/test_auth_routes.py | 61 ++++++++++++++----------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 10be02f..4665a51 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import MagicMock, AsyncMock, patch +from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from uuid import uuid4 @@ -13,30 +13,22 @@ client = TestClient(app) -@pytest.fixture(autouse=True) -def setup_overrides(): - """Preserves and restores dependency_overrides after each test.""" - old_overrides = app.dependency_overrides.copy() - yield - app.dependency_overrides = old_overrides - - @pytest.fixture def mock_db_session(): """Provides a fresh mock database session and sets it in overrides.""" mock_db = MagicMock(spec=Session) # Default behavior for query chain mock_db.query.return_value.filter.return_value.first.return_value = None - app.dependency_overrides[get_db_connection] = lambda: mock_db - return mock_db + with patch.dict(app.dependency_overrides, {get_db_connection: lambda: mock_db}): + yield mock_db @pytest.fixture def mock_current_user(): """Fixture to provide a mock user and set it in overrides.""" user = make_mock_user() - app.dependency_overrides[get_current_user] = lambda: user - return user + with patch.dict(app.dependency_overrides, {get_current_user: lambda: user}): + yield user def make_mock_user(email="test@example.com", full_name="Test User"): @@ -205,18 +197,18 @@ def test_logout_without_token(mock_db_session): def test_github_callback_success(mock_db_session, mock_current_user): """Test successful GitHub OAuth callback linking user and installation.""" - # Patch the httpx module reference in the router to avoid breaking TestClient - with patch("app.routers.auth.httpx") as mock_httpx: - # Configure AsyncMock for the post/get calls - mock_client = mock_httpx.AsyncClient.return_value.__aenter__.return_value - - mock_token_resp = MagicMock() - mock_token_resp.json.return_value = {"access_token": "gh_access_token"} - mock_client.post.return_value = mock_token_resp - - mock_user_resp = MagicMock() - mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - mock_client.get.return_value = mock_user_resp + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = {"access_token": "gh_access_token"} + + mock_user_resp = MagicMock() + mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} + + # Mock the AsyncClient context manager correctly + with patch("app.routers.auth.httpx.AsyncClient") as mock_client_class: + mock_instance = mock_client_class.return_value + mock_instance.__aenter__.return_value = mock_instance + mock_instance.post.return_value = mock_token_resp + mock_instance.get.return_value = mock_user_resp response = client.get("/api/auth/github/callback?code=mock_code&installation_id=67890") @@ -237,15 +229,16 @@ def test_github_callback_missing_code(mock_db_session, mock_current_user): def test_github_callback_github_error(mock_db_session, mock_current_user): """Test handling of error returned by GitHub during token exchange.""" - with patch("app.routers.auth.httpx") as mock_httpx: - mock_client = mock_httpx.AsyncClient.return_value.__aenter__.return_value - - mock_token_resp = MagicMock() - mock_token_resp.json.return_value = { - "error": "bad_verification_code", - "error_description": "The code passed is incorrect or expired." - } - mock_client.post.return_value = mock_token_resp + mock_token_resp = MagicMock() + mock_token_resp.json.return_value = { + "error": "bad_verification_code", + "error_description": "The code passed is incorrect or expired." + } + + with patch("app.routers.auth.httpx.AsyncClient") as mock_client_class: + mock_instance = mock_client_class.return_value + mock_instance.__aenter__.return_value = mock_instance + mock_instance.post.return_value = mock_token_resp response = client.get("/api/auth/github/callback?code=wrong_code") From ee97695feb9aaa64152afcccc0d414bff8e77acf Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:30:17 +0530 Subject: [PATCH 17/19] fix(auth-test): fresh TestClient fixture and patch.dict for robust state management --- tests/routers/test_auth_routes.py | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 4665a51..2b94c1a 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -10,7 +10,11 @@ # =========== Setup =========== -client = TestClient(app) +@pytest.fixture +def client(): + """Provides a fresh TestClient for each test.""" + with TestClient(app) as c: + yield c @pytest.fixture @@ -51,7 +55,7 @@ def make_mock_user(email="test@example.com", full_name="Test User"): # =========== POST /auth/signup Tests =========== -def test_signup_success(mock_db_session): +def test_signup_success(client, mock_db_session): """Test that a new user can register with valid credentials.""" mock_db_session.query.return_value.filter.return_value.first.return_value = None @@ -78,7 +82,7 @@ def test_signup_success(mock_db_session): mock_db_session.commit.assert_called() -def test_signup_duplicate_email(mock_db_session): +def test_signup_duplicate_email(client, mock_db_session): """Test that signup fails with 400 when email already exists.""" existing_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = existing_user @@ -94,7 +98,7 @@ def test_signup_duplicate_email(mock_db_session): mock_db_session.add.assert_not_called() -def test_signup_invalid_email(mock_db_session): +def test_signup_invalid_email(client, mock_db_session): """Test that signup fails with 422 when email format is invalid.""" response = client.post("/api/auth/signup", json={ "email": "not-a-valid-email", @@ -108,7 +112,7 @@ def test_signup_invalid_email(mock_db_session): # =========== POST /auth/login Tests =========== -def test_login_success(mock_db_session): +def test_login_success(client, mock_db_session): """Test that a user with correct credentials receives a successful response.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user @@ -130,7 +134,7 @@ def test_login_success(mock_db_session): mock_db_session.commit.assert_called() -def test_login_wrong_password(mock_db_session): +def test_login_wrong_password(client, mock_db_session): """Test that login fails with 401 when password is incorrect.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user @@ -145,7 +149,7 @@ def test_login_wrong_password(mock_db_session): assert "Incorrect credentials" in response.json()["detail"] -def test_login_user_not_found(mock_db_session): +def test_login_user_not_found(client, mock_db_session): """Test that login fails with 401 when user does not exist.""" mock_db_session.query.return_value.filter.return_value.first.return_value = None @@ -157,7 +161,7 @@ def test_login_user_not_found(mock_db_session): assert response.status_code == 401 -def test_login_missing_fields(mock_db_session): +def test_login_missing_fields(client, mock_db_session): """Test that login fails with 422 when required fields are missing.""" response = client.post("/api/auth/login", json={ "email": "test@example.com" @@ -170,7 +174,7 @@ def test_login_missing_fields(mock_db_session): # =========== POST /auth/logout Tests =========== -def test_logout_with_valid_token(mock_db_session): +def test_logout_with_valid_token(client, mock_db_session): """Test that logout clears cookies and nullifies the refresh token hash.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user @@ -185,7 +189,7 @@ def test_logout_with_valid_token(mock_db_session): mock_db_session.commit.assert_called() -def test_logout_without_token(mock_db_session): +def test_logout_without_token(client, mock_db_session): """Test that logout succeeds gracefully even with no cookies.""" response = client.post("/api/auth/logout") @@ -195,7 +199,7 @@ def test_logout_without_token(mock_db_session): # =========== GET /auth/github/callback Tests =========== -def test_github_callback_success(mock_db_session, mock_current_user): +def test_github_callback_success(client, mock_db_session, mock_current_user): """Test successful GitHub OAuth callback linking user and installation.""" mock_token_resp = MagicMock() mock_token_resp.json.return_value = {"access_token": "gh_access_token"} @@ -203,9 +207,9 @@ def test_github_callback_success(mock_db_session, mock_current_user): mock_user_resp = MagicMock() mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - # Mock the AsyncClient context manager correctly - with patch("app.routers.auth.httpx.AsyncClient") as mock_client_class: - mock_instance = mock_client_class.return_value + # Mock the AsyncClient class as used in the router + with patch("app.routers.auth.httpx.AsyncClient") as MockClient: + mock_instance = MockClient.return_value mock_instance.__aenter__.return_value = mock_instance mock_instance.post.return_value = mock_token_resp mock_instance.get.return_value = mock_user_resp @@ -220,14 +224,14 @@ def test_github_callback_success(mock_db_session, mock_current_user): mock_db_session.commit.assert_called() -def test_github_callback_missing_code(mock_db_session, mock_current_user): +def test_github_callback_missing_code(client, mock_db_session, mock_current_user): """Test that missing authorization code returns 400.""" response = client.get("/api/auth/github/callback?installation_id=67890") assert response.status_code == 400 assert "code missing" in response.json()["detail"].lower() -def test_github_callback_github_error(mock_db_session, mock_current_user): +def test_github_callback_github_error(client, mock_db_session, mock_current_user): """Test handling of error returned by GitHub during token exchange.""" mock_token_resp = MagicMock() mock_token_resp.json.return_value = { @@ -235,8 +239,8 @@ def test_github_callback_github_error(mock_db_session, mock_current_user): "error_description": "The code passed is incorrect or expired." } - with patch("app.routers.auth.httpx.AsyncClient") as mock_client_class: - mock_instance = mock_client_class.return_value + with patch("app.routers.auth.httpx.AsyncClient") as MockClient: + mock_instance = MockClient.return_value mock_instance.__aenter__.return_value = mock_instance mock_instance.post.return_value = mock_token_resp From e233c6e5444f8cd08a1e863ab6b0adcc30e34436 Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:35:15 +0530 Subject: [PATCH 18/19] fix(auth-test): use AsyncClient for async endpoints and patch.dict for robust overrides --- tests/routers/test_auth_routes.py | 32 ++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 2b94c1a..98793f5 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient +from httpx import AsyncClient from uuid import uuid4 from sqlalchemy.orm import Session @@ -8,8 +9,6 @@ from app.deps import get_db_connection, get_current_user from app.models.user import User -# =========== Setup =========== - @pytest.fixture def client(): """Provides a fresh TestClient for each test.""" @@ -17,6 +16,15 @@ def client(): yield c +# =========== Diagnostic Test =========== + +def test_api_root(client): + """Diagnostic test to ensure GET /api works.""" + response = client.get("/api") + assert response.status_code == 200 + assert "Delta" in response.json()["message"] + + @pytest.fixture def mock_db_session(): """Provides a fresh mock database session and sets it in overrides.""" @@ -199,7 +207,8 @@ def test_logout_without_token(client, mock_db_session): # =========== GET /auth/github/callback Tests =========== -def test_github_callback_success(client, mock_db_session, mock_current_user): +@pytest.mark.asyncio +async def test_github_callback_success(mock_db_session, mock_current_user): """Test successful GitHub OAuth callback linking user and installation.""" mock_token_resp = MagicMock() mock_token_resp.json.return_value = {"access_token": "gh_access_token"} @@ -207,14 +216,15 @@ def test_github_callback_success(client, mock_db_session, mock_current_user): mock_user_resp = MagicMock() mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - # Mock the AsyncClient class as used in the router + # Mock matching the way it's used in the router with patch("app.routers.auth.httpx.AsyncClient") as MockClient: mock_instance = MockClient.return_value mock_instance.__aenter__.return_value = mock_instance mock_instance.post.return_value = mock_token_resp mock_instance.get.return_value = mock_user_resp - response = client.get("/api/auth/github/callback?code=mock_code&installation_id=67890") + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.get("/api/auth/github/callback?code=mock_code&installation_id=67890") assert response.status_code == 303 assert "dashboard" in response.headers["location"] @@ -224,14 +234,17 @@ def test_github_callback_success(client, mock_db_session, mock_current_user): mock_db_session.commit.assert_called() -def test_github_callback_missing_code(client, mock_db_session, mock_current_user): +@pytest.mark.asyncio +async def test_github_callback_missing_code(mock_db_session, mock_current_user): """Test that missing authorization code returns 400.""" - response = client.get("/api/auth/github/callback?installation_id=67890") + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.get("/api/auth/github/callback?installation_id=67890") assert response.status_code == 400 assert "code missing" in response.json()["detail"].lower() -def test_github_callback_github_error(client, mock_db_session, mock_current_user): +@pytest.mark.asyncio +async def test_github_callback_github_error(mock_db_session, mock_current_user): """Test handling of error returned by GitHub during token exchange.""" mock_token_resp = MagicMock() mock_token_resp.json.return_value = { @@ -244,7 +257,8 @@ def test_github_callback_github_error(client, mock_db_session, mock_current_user mock_instance.__aenter__.return_value = mock_instance mock_instance.post.return_value = mock_token_resp - response = client.get("/api/auth/github/callback?code=wrong_code") + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.get("/api/auth/github/callback?code=wrong_code") assert response.status_code == 400 assert "GitHub Error" in response.json()["detail"] From cada42926fcdce4ad2c971fe8177a78ac40ffc3b Mon Sep 17 00:00:00 2001 From: Alladasetti Jahnavi Date: Wed, 11 Mar 2026 14:38:21 +0530 Subject: [PATCH 19/19] revert: auth tests to stable state before github callback additions --- tests/routers/test_auth_routes.py | 136 ++++++------------------------ 1 file changed, 25 insertions(+), 111 deletions(-) diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py index 98793f5..ba78537 100644 --- a/tests/routers/test_auth_routes.py +++ b/tests/routers/test_auth_routes.py @@ -1,69 +1,41 @@ import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient -from httpx import AsyncClient from uuid import uuid4 -from sqlalchemy.orm import Session from app.main import app -from app.deps import get_db_connection, get_current_user +from app.deps import get_db_connection from app.models.user import User -@pytest.fixture -def client(): - """Provides a fresh TestClient for each test.""" - with TestClient(app) as c: - yield c - +# =========== Setup =========== -# =========== Diagnostic Test =========== - -def test_api_root(client): - """Diagnostic test to ensure GET /api works.""" - response = client.get("/api") - assert response.status_code == 200 - assert "Delta" in response.json()["message"] +client = TestClient(app) @pytest.fixture def mock_db_session(): - """Provides a fresh mock database session and sets it in overrides.""" - mock_db = MagicMock(spec=Session) - # Default behavior for query chain - mock_db.query.return_value.filter.return_value.first.return_value = None - with patch.dict(app.dependency_overrides, {get_db_connection: lambda: mock_db}): - yield mock_db - - -@pytest.fixture -def mock_current_user(): - """Fixture to provide a mock user and set it in overrides.""" - user = make_mock_user() - with patch.dict(app.dependency_overrides, {get_current_user: lambda: user}): - yield user + """Provides a fresh mock database session and injects it as the FastAPI dependency.""" + mock_db = MagicMock() + app.dependency_overrides[get_db_connection] = lambda: mock_db + yield mock_db + app.dependency_overrides.pop(get_db_connection, None) def make_mock_user(email="test@example.com", full_name="Test User"): - """Helper to create a mock User model instance with valid string attributes.""" + """Helper to create a mock User model instance.""" user = MagicMock(spec=User) user.id = uuid4() - user.email = str(email) - user.full_name = str(full_name) - user.password_hash = "hashed_pw" + user.email = email + user.full_name = full_name + user.password_hash = "hashed_password" user.current_refresh_token_hash = None - user.github_user_id = None - user.github_username = None return user -# Create a default mock user for general use -mock_user = make_mock_user() - - # =========== POST /auth/signup Tests =========== -def test_signup_success(client, mock_db_session): +def test_signup_success(mock_db_session): """Test that a new user can register with valid credentials.""" mock_db_session.query.return_value.filter.return_value.first.return_value = None @@ -90,7 +62,7 @@ def test_signup_success(client, mock_db_session): mock_db_session.commit.assert_called() -def test_signup_duplicate_email(client, mock_db_session): +def test_signup_duplicate_email(mock_db_session): """Test that signup fails with 400 when email already exists.""" existing_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = existing_user @@ -106,7 +78,7 @@ def test_signup_duplicate_email(client, mock_db_session): mock_db_session.add.assert_not_called() -def test_signup_invalid_email(client, mock_db_session): +def test_signup_invalid_email(mock_db_session): """Test that signup fails with 422 when email format is invalid.""" response = client.post("/api/auth/signup", json={ "email": "not-a-valid-email", @@ -120,7 +92,7 @@ def test_signup_invalid_email(client, mock_db_session): # =========== POST /auth/login Tests =========== -def test_login_success(client, mock_db_session): +def test_login_success(mock_db_session): """Test that a user with correct credentials receives a successful response.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user @@ -142,7 +114,7 @@ def test_login_success(client, mock_db_session): mock_db_session.commit.assert_called() -def test_login_wrong_password(client, mock_db_session): +def test_login_wrong_password(mock_db_session): """Test that login fails with 401 when password is incorrect.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user @@ -157,7 +129,7 @@ def test_login_wrong_password(client, mock_db_session): assert "Incorrect credentials" in response.json()["detail"] -def test_login_user_not_found(client, mock_db_session): +def test_login_user_not_found(mock_db_session): """Test that login fails with 401 when user does not exist.""" mock_db_session.query.return_value.filter.return_value.first.return_value = None @@ -169,7 +141,7 @@ def test_login_user_not_found(client, mock_db_session): assert response.status_code == 401 -def test_login_missing_fields(client, mock_db_session): +def test_login_missing_fields(mock_db_session): """Test that login fails with 422 when required fields are missing.""" response = client.post("/api/auth/login", json={ "email": "test@example.com" @@ -182,83 +154,25 @@ def test_login_missing_fields(client, mock_db_session): # =========== POST /auth/logout Tests =========== -def test_logout_with_valid_token(client, mock_db_session): +def test_logout_with_valid_token(mock_db_session): """Test that logout clears cookies and nullifies the refresh token hash.""" mock_user = make_mock_user() mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user with patch("app.routers.auth.security.verify_token", return_value={"sub": str(mock_user.id)}): - client.cookies.set("access_token", "valid_access_token") - response = client.post("/api/auth/logout") - client.cookies.delete("access_token") + response = client.post( + "/api/auth/logout", + cookies={"access_token": "valid_access_token"} + ) assert response.status_code == 200 assert response.json()["message"] == "Logout successful" mock_db_session.commit.assert_called() -def test_logout_without_token(client, mock_db_session): +def test_logout_without_token(mock_db_session): """Test that logout succeeds gracefully even with no cookies.""" response = client.post("/api/auth/logout") assert response.status_code == 200 assert response.json()["message"] == "Logout successful" - - -# =========== GET /auth/github/callback Tests =========== - -@pytest.mark.asyncio -async def test_github_callback_success(mock_db_session, mock_current_user): - """Test successful GitHub OAuth callback linking user and installation.""" - mock_token_resp = MagicMock() - mock_token_resp.json.return_value = {"access_token": "gh_access_token"} - - mock_user_resp = MagicMock() - mock_user_resp.json.return_value = {"id": 12345, "login": "github_user"} - - # Mock matching the way it's used in the router - with patch("app.routers.auth.httpx.AsyncClient") as MockClient: - mock_instance = MockClient.return_value - mock_instance.__aenter__.return_value = mock_instance - mock_instance.post.return_value = mock_token_resp - mock_instance.get.return_value = mock_user_resp - - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get("/api/auth/github/callback?code=mock_code&installation_id=67890") - - assert response.status_code == 303 - assert "dashboard" in response.headers["location"] - - assert mock_current_user.github_user_id == 12345 - assert mock_current_user.github_username == "github_user" - mock_db_session.commit.assert_called() - - -@pytest.mark.asyncio -async def test_github_callback_missing_code(mock_db_session, mock_current_user): - """Test that missing authorization code returns 400.""" - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get("/api/auth/github/callback?installation_id=67890") - assert response.status_code == 400 - assert "code missing" in response.json()["detail"].lower() - - -@pytest.mark.asyncio -async def test_github_callback_github_error(mock_db_session, mock_current_user): - """Test handling of error returned by GitHub during token exchange.""" - mock_token_resp = MagicMock() - mock_token_resp.json.return_value = { - "error": "bad_verification_code", - "error_description": "The code passed is incorrect or expired." - } - - with patch("app.routers.auth.httpx.AsyncClient") as MockClient: - mock_instance = MockClient.return_value - mock_instance.__aenter__.return_value = mock_instance - mock_instance.post.return_value = mock_token_resp - - async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get("/api/auth/github/callback?code=wrong_code") - - assert response.status_code == 400 - assert "GitHub Error" in response.json()["detail"]