Skip to content

Commit e4c216e

Browse files
committed
feat(api): integrate API versioning in backend routes
- main.py: import API_PREFIX, apply to all routers - Route files: remove hardcoded /api, use relative paths - auth.py: /api/auth → /auth - repos.py: /api/repos → /repos - search.py: /api → (empty) - analysis.py: /api/repos → /repos - api_keys.py: /api → (empty) - playground.py: /api/playground → /playground - WebSocket endpoint now versioned: /api/v1/ws/index/{repo_id} - Health endpoint stays at root: /health (no versioning) - Tests: import API_PREFIX, all paths now use config All 13 tests passing. Part of #55
1 parent 7088492 commit e4c216e

8 files changed

Lines changed: 37 additions & 28 deletions

File tree

backend/main.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from starlette.responses import JSONResponse
1111
import os
1212

13+
# Import API config (single source of truth for versioning)
14+
from config.api import API_PREFIX, API_VERSION
15+
1316
# Import routers
1417
from routes.auth import router as auth_router
1518
from routes.health import router as health_router
@@ -68,17 +71,20 @@ async def dispatch(self, request: Request, call_next):
6871

6972

7073
# ===== ROUTERS =====
71-
72-
app.include_router(health_router)
73-
app.include_router(auth_router)
74-
app.include_router(playground_router)
75-
app.include_router(repos_router)
76-
app.include_router(search_router)
77-
app.include_router(analysis_router)
78-
app.include_router(api_keys_router)
79-
80-
# WebSocket endpoint (can't be in router easily)
81-
app.add_api_websocket_route("/ws/index/{repo_id}", websocket_index)
74+
# All API routes are prefixed with API_PREFIX (e.g., /api/v1)
75+
# Route files define their sub-path (e.g., /auth, /repos)
76+
# Final paths: /api/v1/auth, /api/v1/repos, etc.
77+
78+
app.include_router(health_router) # /health stays at root (no versioning needed)
79+
app.include_router(auth_router, prefix=API_PREFIX)
80+
app.include_router(playground_router, prefix=API_PREFIX)
81+
app.include_router(repos_router, prefix=API_PREFIX)
82+
app.include_router(search_router, prefix=API_PREFIX)
83+
app.include_router(analysis_router, prefix=API_PREFIX)
84+
app.include_router(api_keys_router, prefix=API_PREFIX)
85+
86+
# WebSocket endpoint (versioned)
87+
app.add_api_websocket_route(f"{API_PREFIX}/ws/index/{{repo_id}}", websocket_index)
8288

8389

8490
# ===== ERROR HANDLERS =====

backend/routes/analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from services.input_validator import InputValidator
1010
from middleware.auth import require_auth, AuthContext
1111

12-
router = APIRouter(prefix="/api/repos", tags=["Analysis"])
12+
router = APIRouter(prefix="/repos", tags=["Analysis"])
1313

1414

1515
class ImpactRequest(BaseModel):

backend/routes/api_keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dependencies import api_key_manager, rate_limiter, metrics
66
from middleware.auth import require_auth, AuthContext
77

8-
router = APIRouter(prefix="/api", tags=["API Keys"])
8+
router = APIRouter(prefix="", tags=["API Keys"])
99

1010

1111
class CreateAPIKeyRequest(BaseModel):

backend/routes/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from middleware.auth import get_current_user
1010

1111
# Create router
12-
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
12+
router = APIRouter(prefix="/auth", tags=["Authentication"])
1313

1414

1515
# Request/Response Models

backend/routes/playground.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from dependencies import indexer, cache, repo_manager
88
from services.input_validator import InputValidator
99

10-
router = APIRouter(prefix="/api/playground", tags=["Playground"])
10+
router = APIRouter(prefix="/playground", tags=["Playground"])
1111

1212
# Demo repo mapping (populated on startup)
1313
DEMO_REPO_IDS = {}

backend/routes/repos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from services.input_validator import InputValidator
1414
from middleware.auth import require_auth, AuthContext
1515

16-
router = APIRouter(prefix="/api/repos", tags=["Repositories"])
16+
router = APIRouter(prefix="/repos", tags=["Repositories"])
1717

1818

1919
class AddRepoRequest(BaseModel):

backend/routes/search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from services.input_validator import InputValidator
1212
from middleware.auth import require_auth, AuthContext
1313

14-
router = APIRouter(prefix="/api", tags=["Search"])
14+
router = APIRouter(prefix="", tags=["Search"])
1515

1616

1717
class SearchRequest(BaseModel):

backend/tests/test_api.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"""
55
import pytest
66

7+
# Import API prefix from centralized config (single source of truth)
8+
from config.api import API_PREFIX
9+
710

811
class TestAPIAuthentication:
912
"""Test authentication and authorization"""
@@ -15,20 +18,20 @@ def test_health_check_no_auth_required(self, client_no_auth):
1518

1619
def test_protected_endpoint_requires_auth(self, client_no_auth):
1720
"""Protected endpoints should require API key"""
18-
response = client_no_auth.get("/api/repos")
21+
response = client_no_auth.get(f"{API_PREFIX}/repos")
1922
assert response.status_code in [401, 403] # Either unauthorized or forbidden
2023

2124
def test_valid_dev_key_works(self, client_no_auth, valid_headers):
2225
"""Valid development API key should work in debug mode"""
2326
# Note: This tests actual auth, requires DEBUG=true and DEV_API_KEY set
24-
response = client_no_auth.get("/api/repos", headers=valid_headers)
27+
response = client_no_auth.get(f"{API_PREFIX}/repos", headers=valid_headers)
2528
# May return 200 or 401 depending on env setup during test
2629
assert response.status_code in [200, 401]
2730

2831
def test_invalid_key_rejected(self, client_no_auth):
2932
"""Invalid API keys should be rejected"""
3033
response = client_no_auth.get(
31-
"/api/repos",
34+
f"{API_PREFIX}/repos",
3235
headers={"Authorization": "Bearer invalid-random-key"}
3336
)
3437
assert response.status_code in [401, 403]
@@ -41,7 +44,7 @@ def test_reject_file_scheme_urls(self, client, valid_headers, malicious_payloads
4144
"""Should block file:// URLs"""
4245
for url in malicious_payloads["file_urls"]:
4346
response = client.post(
44-
"/api/repos",
47+
f"{API_PREFIX}/repos",
4548
headers=valid_headers,
4649
json={"name": "test", "git_url": url}
4750
)
@@ -52,7 +55,7 @@ def test_reject_localhost_urls(self, client, valid_headers, malicious_payloads):
5255
"""Should block localhost/private IP URLs"""
5356
for url in malicious_payloads["localhost_urls"]:
5457
response = client.post(
55-
"/api/repos",
58+
f"{API_PREFIX}/repos",
5659
headers=valid_headers,
5760
json={"name": "test", "git_url": url}
5861
)
@@ -65,7 +68,7 @@ def test_reject_invalid_repo_names(self, client, valid_headers):
6568

6669
for name in invalid_names:
6770
response = client.post(
68-
"/api/repos",
71+
f"{API_PREFIX}/repos",
6972
headers=valid_headers,
7073
json={"name": name, "git_url": "https://github.com/test/repo"}
7174
)
@@ -79,7 +82,7 @@ def test_reject_sql_injection_attempts(self, client, valid_headers, malicious_pa
7982
"""Should block SQL injection in search queries"""
8083
for sql_query in malicious_payloads["sql_injection"]:
8184
response = client.post(
82-
"/api/search",
85+
f"{API_PREFIX}/search",
8386
headers=valid_headers,
8487
json={"query": sql_query, "repo_id": "test-id"}
8588
)
@@ -90,7 +93,7 @@ def test_reject_sql_injection_attempts(self, client, valid_headers, malicious_pa
9093
def test_reject_empty_queries(self, client, valid_headers):
9194
"""Should reject empty search queries"""
9295
response = client.post(
93-
"/api/search",
96+
f"{API_PREFIX}/search",
9497
headers=valid_headers,
9598
json={"query": "", "repo_id": "test-id"}
9699
)
@@ -100,7 +103,7 @@ def test_reject_empty_queries(self, client, valid_headers):
100103
def test_reject_oversized_queries(self, client, valid_headers):
101104
"""Should reject queries over max length"""
102105
response = client.post(
103-
"/api/search",
106+
f"{API_PREFIX}/search",
104107
headers=valid_headers,
105108
json={"query": "a" * 1000, "repo_id": "test-id"}
106109
)
@@ -115,7 +118,7 @@ def test_reject_path_traversal_attempts(self, client, valid_headers, malicious_p
115118
"""Should block path traversal in impact analysis"""
116119
for path in malicious_payloads["path_traversal"]:
117120
response = client.post(
118-
"/api/repos/test-id/impact",
121+
f"{API_PREFIX}/repos/test-id/impact",
119122
headers=valid_headers,
120123
json={"repo_id": "test-id", "file_path": path}
121124
)
@@ -141,7 +144,7 @@ def test_max_limits_configured(self):
141144
def test_search_results_capped(self, client, valid_headers):
142145
"""Search results should be capped at maximum"""
143146
response = client.post(
144-
"/api/search",
147+
f"{API_PREFIX}/search",
145148
headers=valid_headers,
146149
json={
147150
"query": "test query",

0 commit comments

Comments
 (0)