Skip to content

Commit 91c8fe9

Browse files
Merge pull request #122 from Delta-Docs/backend-integration-tests
test(backend): add comprehensive endpoint integration tests for repos…
2 parents 1c2ba6d + cada429 commit 91c8fe9

4 files changed

Lines changed: 854 additions & 0 deletions

File tree

tests/routers/test_auth_routes.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import pytest
2+
from unittest.mock import MagicMock, patch
3+
from fastapi.testclient import TestClient
4+
from uuid import uuid4
5+
6+
from app.main import app
7+
from app.deps import get_db_connection
8+
from app.models.user import User
9+
10+
# =========== Setup ===========
11+
12+
client = TestClient(app)
13+
14+
15+
@pytest.fixture
16+
def mock_db_session():
17+
"""Provides a fresh mock database session and injects it as the FastAPI dependency."""
18+
mock_db = MagicMock()
19+
app.dependency_overrides[get_db_connection] = lambda: mock_db
20+
yield mock_db
21+
app.dependency_overrides.pop(get_db_connection, None)
22+
23+
24+
def make_mock_user(email="test@example.com", full_name="Test User"):
25+
"""Helper to create a mock User model instance."""
26+
user = MagicMock(spec=User)
27+
user.id = uuid4()
28+
user.email = email
29+
user.full_name = full_name
30+
user.password_hash = "hashed_password"
31+
user.current_refresh_token_hash = None
32+
return user
33+
34+
35+
# =========== POST /auth/signup Tests ===========
36+
37+
38+
def test_signup_success(mock_db_session):
39+
"""Test that a new user can register with valid credentials."""
40+
mock_db_session.query.return_value.filter.return_value.first.return_value = None
41+
42+
mock_user = make_mock_user()
43+
44+
with patch("app.routers.auth.security.get_hash", return_value="hashed_pw"), \
45+
patch("app.routers.auth.security.create_access_token", return_value="mock_access_token"), \
46+
patch("app.routers.auth.security.create_refresh_token", return_value="mock_refresh_token"), \
47+
patch("app.routers.auth.User") as MockUser:
48+
49+
MockUser.return_value = mock_user
50+
51+
response = client.post("/api/auth/signup", json={
52+
"email": "test@example.com",
53+
"full_name": "Test User",
54+
"password": "securepassword123"
55+
})
56+
57+
assert response.status_code == 200
58+
data = response.json()
59+
assert data["email"] == "test@example.com"
60+
assert data["name"] == "Test User"
61+
mock_db_session.add.assert_called_once()
62+
mock_db_session.commit.assert_called()
63+
64+
65+
def test_signup_duplicate_email(mock_db_session):
66+
"""Test that signup fails with 400 when email already exists."""
67+
existing_user = make_mock_user()
68+
mock_db_session.query.return_value.filter.return_value.first.return_value = existing_user
69+
70+
response = client.post("/api/auth/signup", json={
71+
"email": "test@example.com",
72+
"full_name": "Test User",
73+
"password": "securepassword123"
74+
})
75+
76+
assert response.status_code == 400
77+
assert "already exists" in response.json()["detail"]
78+
mock_db_session.add.assert_not_called()
79+
80+
81+
def test_signup_invalid_email(mock_db_session):
82+
"""Test that signup fails with 422 when email format is invalid."""
83+
response = client.post("/api/auth/signup", json={
84+
"email": "not-a-valid-email",
85+
"full_name": "Test User",
86+
"password": "securepassword123"
87+
})
88+
89+
assert response.status_code == 422
90+
91+
92+
# =========== POST /auth/login Tests ===========
93+
94+
95+
def test_login_success(mock_db_session):
96+
"""Test that a user with correct credentials receives a successful response."""
97+
mock_user = make_mock_user()
98+
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user
99+
100+
with patch("app.routers.auth.security.verify_hash", return_value=True), \
101+
patch("app.routers.auth.security.create_access_token", return_value="access_tok"), \
102+
patch("app.routers.auth.security.create_refresh_token", return_value="refresh_tok"), \
103+
patch("app.routers.auth.security.get_hash", return_value="hashed_refresh"):
104+
105+
response = client.post("/api/auth/login", json={
106+
"email": "test@example.com",
107+
"password": "correctpassword"
108+
})
109+
110+
assert response.status_code == 200
111+
data = response.json()
112+
assert data["email"] == "test@example.com"
113+
assert data["name"] == "Test User"
114+
mock_db_session.commit.assert_called()
115+
116+
117+
def test_login_wrong_password(mock_db_session):
118+
"""Test that login fails with 401 when password is incorrect."""
119+
mock_user = make_mock_user()
120+
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user
121+
122+
with patch("app.routers.auth.security.verify_hash", return_value=False):
123+
response = client.post("/api/auth/login", json={
124+
"email": "test@example.com",
125+
"password": "wrongpassword"
126+
})
127+
128+
assert response.status_code == 401
129+
assert "Incorrect credentials" in response.json()["detail"]
130+
131+
132+
def test_login_user_not_found(mock_db_session):
133+
"""Test that login fails with 401 when user does not exist."""
134+
mock_db_session.query.return_value.filter.return_value.first.return_value = None
135+
136+
response = client.post("/api/auth/login", json={
137+
"email": "nobody@example.com",
138+
"password": "anypassword"
139+
})
140+
141+
assert response.status_code == 401
142+
143+
144+
def test_login_missing_fields(mock_db_session):
145+
"""Test that login fails with 422 when required fields are missing."""
146+
response = client.post("/api/auth/login", json={
147+
"email": "test@example.com"
148+
# missing password
149+
})
150+
151+
assert response.status_code == 422
152+
153+
154+
# =========== POST /auth/logout Tests ===========
155+
156+
157+
def test_logout_with_valid_token(mock_db_session):
158+
"""Test that logout clears cookies and nullifies the refresh token hash."""
159+
mock_user = make_mock_user()
160+
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user
161+
162+
with patch("app.routers.auth.security.verify_token", return_value={"sub": str(mock_user.id)}):
163+
response = client.post(
164+
"/api/auth/logout",
165+
cookies={"access_token": "valid_access_token"}
166+
)
167+
168+
assert response.status_code == 200
169+
assert response.json()["message"] == "Logout successful"
170+
mock_db_session.commit.assert_called()
171+
172+
173+
def test_logout_without_token(mock_db_session):
174+
"""Test that logout succeeds gracefully even with no cookies."""
175+
response = client.post("/api/auth/logout")
176+
177+
assert response.status_code == 200
178+
assert response.json()["message"] == "Logout successful"
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import pytest
2+
from unittest.mock import MagicMock, AsyncMock, patch
3+
from fastapi.testclient import TestClient
4+
from uuid import uuid4
5+
6+
from app.main import app
7+
from app.deps import get_db_connection, get_current_user
8+
from app.models.user import User
9+
10+
# =========== Setup ===========
11+
12+
client = TestClient(app)
13+
14+
# Create a mock authenticated user for all dashboard tests
15+
mock_user_id = uuid4()
16+
mock_user = MagicMock(spec=User)
17+
mock_user.id = mock_user_id
18+
mock_user.email = "tester@delta.com"
19+
mock_user.full_name = "Jahnavi Tester"
20+
21+
22+
def override_get_current_user():
23+
return mock_user
24+
25+
26+
app.dependency_overrides[get_current_user] = override_get_current_user
27+
28+
29+
@pytest.fixture
30+
def mock_db_session():
31+
"""Provides a fresh mock database session injected as a FastAPI dependency."""
32+
mock_db = MagicMock()
33+
app.dependency_overrides[get_db_connection] = lambda: mock_db
34+
yield mock_db
35+
app.dependency_overrides.pop(get_db_connection, None)
36+
37+
38+
# =========== GET /dashboard/stats Tests ===========
39+
40+
41+
def test_get_dashboard_stats_success(mock_db_session):
42+
"""Test that dashboard stats returns correct counts for the authenticated user."""
43+
mock_db_session.query.return_value.filter.return_value.scalar.return_value = 3
44+
mock_db_session.query.return_value.join.return_value.filter.return_value.scalar.return_value = 7
45+
mock_db_session.query.return_value.join.return_value.join.return_value.filter.return_value.scalar.return_value = 21
46+
47+
response = client.get("/api/dashboard/stats")
48+
49+
assert response.status_code == 200
50+
data = response.json()
51+
assert "installations_count" in data
52+
assert "repos_linked_count" in data
53+
assert "drift_events_count" in data
54+
assert "pr_waiting_count" in data
55+
56+
57+
def test_get_dashboard_stats_all_zero(mock_db_session):
58+
"""Test that dashboard stats returns zeros for a new user with no data."""
59+
mock_db_session.query.return_value.filter.return_value.scalar.return_value = 0
60+
mock_db_session.query.return_value.join.return_value.filter.return_value.scalar.return_value = 0
61+
mock_db_session.query.return_value.join.return_value.join.return_value.filter.return_value.scalar.return_value = 0
62+
63+
response = client.get("/api/dashboard/stats")
64+
65+
assert response.status_code == 200
66+
data = response.json()
67+
assert data["installations_count"] == 0
68+
assert data["repos_linked_count"] == 0
69+
assert data["drift_events_count"] == 0
70+
assert data["pr_waiting_count"] == 0
71+
72+
73+
def test_get_dashboard_stats_requires_auth():
74+
"""Test that dashboard stats requires authentication."""
75+
app.dependency_overrides.pop(get_current_user, None)
76+
77+
response = client.get("/api/dashboard/stats")
78+
79+
# Restore override
80+
app.dependency_overrides[get_current_user] = override_get_current_user
81+
82+
assert response.status_code in [401, 403]
83+
84+
85+
# =========== GET /dashboard/repos Tests ===========
86+
87+
88+
def test_get_dashboard_repos_success(mock_db_session):
89+
"""Test that dashboard repos returns repo details for the authenticated user."""
90+
mock_repo = MagicMock()
91+
mock_repo.repo_name = "owner/delta-docs"
92+
mock_repo.installation_id = 101
93+
94+
mock_db_session.query.return_value.join.return_value.filter.return_value \
95+
.order_by.return_value.limit.return_value.all.return_value = [mock_repo]
96+
97+
with patch("app.routers.dashboard.get_repo_details", new_callable=AsyncMock) as mock_details:
98+
mock_details.return_value = {
99+
"name": "delta-docs",
100+
"description": "Documentation drift detector",
101+
"language": "Python",
102+
"stargazers_count": 42,
103+
"forks_count": 5,
104+
"avatar_url": None,
105+
}
106+
response = client.get("/api/dashboard/repos")
107+
108+
assert response.status_code == 200
109+
data = response.json()
110+
assert len(data) == 1
111+
assert data[0]["name"] == "delta-docs"
112+
assert data[0]["language"] == "Python"
113+
assert data[0]["stargazers_count"] == 42
114+
115+
116+
def test_get_dashboard_repos_empty(mock_db_session):
117+
"""Test that dashboard repos returns empty list when user has no repos."""
118+
mock_db_session.query.return_value.join.return_value.filter.return_value \
119+
.order_by.return_value.limit.return_value.all.return_value = []
120+
121+
response = client.get("/api/dashboard/repos")
122+
123+
assert response.status_code == 200
124+
assert response.json() == []
125+
126+
127+
def test_get_dashboard_repos_github_api_failure(mock_db_session):
128+
"""Test that dashboard repos falls back gracefully when GitHub API fails."""
129+
mock_repo = MagicMock()
130+
mock_repo.repo_name = "owner/delta-docs"
131+
mock_repo.installation_id = 101
132+
133+
mock_db_session.query.return_value.join.return_value.filter.return_value \
134+
.order_by.return_value.limit.return_value.all.return_value = [mock_repo]
135+
136+
with patch("app.routers.dashboard.get_repo_details", side_effect=Exception("GitHub API down")):
137+
response = client.get("/api/dashboard/repos")
138+
139+
assert response.status_code == 200
140+
data = response.json()
141+
assert len(data) == 1
142+
assert data[0]["description"] == "Error fetching details"
143+
assert data[0]["name"] == "owner/delta-docs"

0 commit comments

Comments
 (0)