diff --git a/tests/routers/test_auth_routes.py b/tests/routers/test_auth_routes.py new file mode 100644 index 0000000..ba78537 --- /dev/null +++ b/tests/routers/test_auth_routes.py @@ -0,0 +1,178 @@ +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 +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.""" + 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" diff --git a/tests/routers/test_notification_routes.py b/tests/routers/test_notification_routes.py new file mode 100644 index 0000000..a51c38a --- /dev/null +++ b/tests/routers/test_notification_routes.py @@ -0,0 +1,163 @@ +import pytest +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 +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 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 + + +# =========== 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() diff --git a/tests/routers/test_repos_routes.py b/tests/routers/test_repos_routes.py new file mode 100644 index 0000000..bf8e9cb --- /dev/null +++ b/tests/routers/test_repos_routes.py @@ -0,0 +1,370 @@ +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.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"] is 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 + + +# =========== 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), + overall_drift_score=None, + error_message=None, + started_at=None, + completed_at=None, + ) + + 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.""" + 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" +