diff --git a/backend/config/api.py b/backend/config/api.py index 4bff8ef..1c2a770 100644 --- a/backend/config/api.py +++ b/backend/config/api.py @@ -5,15 +5,11 @@ Example: "v1" -> "v2" will change /api/v1/* to /api/v2/* """ -# ============================================================================= # API VERSION CONFIGURATION -# ============================================================================= API_VERSION = "v1" -# ============================================================================= # DERIVED PREFIXES (auto-calculated from version) -# ============================================================================= # Current versioned API prefix: /api/v1 API_PREFIX = f"/api/{API_VERSION}" @@ -22,9 +18,7 @@ # Routes here will be deprecated but still functional LEGACY_API_PREFIX = "/api" -# ============================================================================= # DEPRECATION SETTINGS -# ============================================================================= # When True, legacy routes (/api/*) will include deprecation warning headers LEGACY_DEPRECATION_ENABLED = True @@ -33,9 +27,7 @@ DEPRECATION_HEADER = "X-API-Deprecated" DEPRECATION_MESSAGE = f"This endpoint is deprecated. Please use {API_PREFIX} instead." -# ============================================================================= # HELPER FUNCTIONS -# ============================================================================= def get_versioned_prefix() -> str: """Get the current versioned API prefix.""" diff --git a/backend/dependencies.py b/backend/dependencies.py index 6dd84e5..26937c9 100644 --- a/backend/dependencies.py +++ b/backend/dependencies.py @@ -3,10 +3,6 @@ All route modules import from here to avoid circular imports. """ from fastapi import HTTPException, Depends -from dotenv import load_dotenv - -# Load env vars first -load_dotenv() from services.indexer_optimized import OptimizedCodeIndexer from services.repo_manager import RepositoryManager diff --git a/backend/main.py b/backend/main.py index ea5eb45..4eafc49 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,10 @@ CodeIntel Backend API FastAPI backend for codebase intelligence """ +# Load env vars once, before any service reads them +from dotenv import load_dotenv +load_dotenv() + from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware @@ -51,8 +55,7 @@ async def lifespan(app: FastAPI): ) -# ===== MIDDLEWARE ===== - +# MIDDLEWARE class RequestSizeLimitMiddleware(BaseHTTPMiddleware): """Limit request body size to prevent abuse.""" MAX_REQUEST_SIZE = 10 * 1024 * 1024 # 10MB @@ -81,7 +84,7 @@ async def dispatch(self, request: Request, call_next): ) -# ===== ROUTERS ===== +# ROUTERS # All API routes are prefixed with API_PREFIX (e.g., /api/v1) # Route files define their sub-path (e.g., /auth, /repos) # Final paths: /api/v1/auth, /api/v1/repos, etc. @@ -104,8 +107,7 @@ async def dispatch(self, request: Request, call_next): app.add_api_websocket_route(f"{API_PREFIX}/ws/repos/{{repo_id}}/indexing", websocket_repo_indexing) -# ===== ERROR HANDLERS ===== - +# ERROR HANDLERS @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Handle validation errors with clear messages.""" diff --git a/backend/middleware/auth.py b/backend/middleware/auth.py index d8a3a1e..7aff83a 100644 --- a/backend/middleware/auth.py +++ b/backend/middleware/auth.py @@ -29,9 +29,7 @@ async def demo_search(auth: AuthContext = Depends(public_auth)): from fastapi.security.http import HTTPAuthorizationCredentials -# --------------------------------------------------------------------------- # Auth Context - unified return type for all auth methods -# --------------------------------------------------------------------------- @dataclass class AuthContext: @@ -52,17 +50,13 @@ def identifier(self) -> str: return self.user_id or self.api_key_name or "anonymous" -# --------------------------------------------------------------------------- # Bearer token scheme (auto_error=False allows optional auth) -# --------------------------------------------------------------------------- _bearer = HTTPBearer(auto_error=False) _bearer_required = HTTPBearer(auto_error=True) -# --------------------------------------------------------------------------- # Core validation functions -# --------------------------------------------------------------------------- def _validate_jwt(token: str) -> Optional[AuthContext]: """Validate Supabase JWT token""" @@ -143,9 +137,7 @@ def _authenticate(token: str) -> AuthContext: ) -# --------------------------------------------------------------------------- # FastAPI Dependencies - use these in your routes -# --------------------------------------------------------------------------- async def require_auth( credentials: HTTPAuthorizationCredentials = Depends(_bearer_required) @@ -177,9 +169,7 @@ async def public_auth( return AuthContext(is_public=True) -# --------------------------------------------------------------------------- # Legacy functions - kept for backwards compatibility -# --------------------------------------------------------------------------- async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(_bearer_required) diff --git a/backend/routes/playground.py b/backend/routes/playground.py index c3dda66..e7ccacf 100644 --- a/backend/routes/playground.py +++ b/backend/routes/playground.py @@ -785,9 +785,7 @@ async def validate_github_repo(request: ValidateRepoRequest, req: Request): return result -# ============================================================================= # Anonymous Indexing Endpoint (#125) -# ============================================================================= @router.post("/index", status_code=202) async def start_anonymous_indexing( @@ -1030,9 +1028,7 @@ async def start_anonymous_indexing( return response_data -# ============================================================================= # GET /playground/index/{job_id} - Check indexing job status (#126) -# ============================================================================= @router.get( "/index/{job_id}", diff --git a/backend/services/cache.py b/backend/services/cache.py index 0ff2790..e6d6bff 100644 --- a/backend/services/cache.py +++ b/backend/services/cache.py @@ -7,12 +7,9 @@ import hashlib from typing import Optional, List, Dict import os -from dotenv import load_dotenv from services.observability import logger, metrics -load_dotenv() - # Configuration REDIS_URL = os.getenv("REDIS_URL") # Railway/Cloud Redis URL REDIS_HOST = os.getenv("REDIS_HOST", "localhost") diff --git a/backend/services/dependency_analyzer.py b/backend/services/dependency_analyzer.py index bad75a8..8dddcac 100644 --- a/backend/services/dependency_analyzer.py +++ b/backend/services/dependency_analyzer.py @@ -399,8 +399,7 @@ def _find_test_files(self, file_path: str, nodes: List[Dict]) -> List[str]: return test_files - # ===== SUPABASE CACHING ===== - + # SUPABASE CACHING def save_to_cache(self, repo_id: str, graph_data: Dict): """Save dependency graph to Supabase for caching""" from services.supabase_service import get_supabase_service diff --git a/backend/services/indexer_optimized.py b/backend/services/indexer_optimized.py index 9317488..c364f5a 100644 --- a/backend/services/indexer_optimized.py +++ b/backend/services/indexer_optimized.py @@ -27,7 +27,6 @@ # Utils import hashlib -from dotenv import load_dotenv import time # Search enhancement @@ -39,8 +38,6 @@ # Observability from services.observability import logger, trace_operation, track_time, capture_exception, add_breadcrumb, metrics -load_dotenv() - # Configuration # Note: If using existing Pinecone index, match the dimension (1536 for small, 3072 for large) EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small") diff --git a/backend/services/observability.py b/backend/services/observability.py index 3ca1c82..645ceec 100644 --- a/backend/services/observability.py +++ b/backend/services/observability.py @@ -30,9 +30,7 @@ async def index_repo(repo_id: str): LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO" if IS_PRODUCTION else "DEBUG") -# ============================================================================= # STRUCTURED LOGGER -# ============================================================================= class StructuredLogger: """ @@ -112,9 +110,7 @@ def critical(self, message: str, **kwargs): logger = StructuredLogger() -# ============================================================================= # SENTRY INTEGRATION HELPERS -# ============================================================================= def set_operation_context(operation: str, **kwargs): """ @@ -191,9 +187,7 @@ def capture_message(message: str, level: str = "info", **context): pass -# ============================================================================= # PERFORMANCE TRACKING -# ============================================================================= @contextmanager def track_time(operation: str, **tags): @@ -307,9 +301,7 @@ def sync_wrapper(*args, **kwargs): return decorator -# ============================================================================= # SIMPLE METRICS (in-memory counters) -# ============================================================================= class Metrics: """ diff --git a/backend/services/playground_limiter.py b/backend/services/playground_limiter.py index 91f595d..817c52b 100644 --- a/backend/services/playground_limiter.py +++ b/backend/services/playground_limiter.py @@ -25,9 +25,7 @@ from services.sentry import capture_exception -# ============================================================================= # DATA CLASSES -# ============================================================================= @dataclass class PlaygroundLimitResult: @@ -134,9 +132,7 @@ def _truncate_id(session_id: str) -> str: return session_id -# ============================================================================= # MAIN CLASS -# ============================================================================= class PlaygroundLimiter: """ @@ -161,9 +157,7 @@ class PlaygroundLimiter: has_repo = limiter.has_indexed_repo(session_token) """ - # ------------------------------------------------------------------------- # Configuration - # ------------------------------------------------------------------------- # Rate limits SESSION_LIMIT_PER_DAY = 50 # Per device (generous for conversion) @@ -198,9 +192,7 @@ def __init__(self, redis_client=None): """ self.redis = redis_client - # ------------------------------------------------------------------------- # Session Data Methods (#127) - # ------------------------------------------------------------------------- def get_session_data(self, session_token: Optional[str]) -> SessionData: """ @@ -449,9 +441,7 @@ def create_session(self, session_token: str) -> bool: capture_exception(e, operation="create_session") return False - # ------------------------------------------------------------------------- # Rate Limiting Methods (existing, updated for hash storage) - # ------------------------------------------------------------------------- def check_limit( self, @@ -657,9 +647,7 @@ def _check_ip_limit(self, client_ip: str, record: bool) -> Tuple[bool, int]: logger.error("IP limit check failed", error=str(e)) return True, self.IP_LIMIT_PER_DAY # Fail open - # ------------------------------------------------------------------------- # Helper Methods - # ------------------------------------------------------------------------- def _get_midnight_utc(self) -> datetime: """Get next midnight UTC for reset time.""" @@ -788,9 +776,7 @@ def get_usage_stats(self) -> dict: return {"error": str(e), "redis_available": False} -# ============================================================================= # SINGLETON -# ============================================================================= _playground_limiter: Optional[PlaygroundLimiter] = None diff --git a/backend/services/sentry.py b/backend/services/sentry.py index 4f7dbdc..bfbddc5 100644 --- a/backend/services/sentry.py +++ b/backend/services/sentry.py @@ -19,7 +19,7 @@ def init_sentry() -> bool: sentry_dsn = os.getenv("SENTRY_DSN") if not sentry_dsn: - print("ℹ️ Sentry DSN not configured - error tracking disabled") + print("[INFO] Sentry DSN not configured - error tracking disabled") return False try: @@ -61,14 +61,14 @@ def init_sentry() -> bool: include_local_variables=True, ) - print(f"✅ Sentry initialized (environment: {environment})") + print(f"[OK] Sentry initialized (environment: {environment})") return True except ImportError: - print("⚠️ sentry-sdk not installed - error tracking disabled") + print("[WARN] sentry-sdk not installed - error tracking disabled") return False except Exception as e: - print(f"⚠️ Failed to initialize Sentry: {e}") + print(f"[WARN] Failed to initialize Sentry: {e}") return False @@ -97,9 +97,7 @@ def _filter_events(event, hint): return event -# ============================================================================ # LEGACY FUNCTIONS - Use observability module for new code -# ============================================================================ def set_user_context(user_id: Optional[str] = None, email: Optional[str] = None): """ diff --git a/backend/services/style_analyzer.py b/backend/services/style_analyzer.py index 28a584b..780496d 100644 --- a/backend/services/style_analyzer.py +++ b/backend/services/style_analyzer.py @@ -3,7 +3,7 @@ Analyzes team coding patterns, naming conventions, and best practices """ from pathlib import Path -from typing import Dict, List, Set +from typing import Dict, List, Optional, Set from collections import defaultdict, Counter import re @@ -137,7 +137,7 @@ def _check_type_hints(self, source_code: str, language: str) -> bool: if language == 'python': return '->' in source_code or ': ' in source_code else: - return ': ' in source_code and 'interface' in source_code or 'type ' in source_code + return ': ' in source_code and ('interface' in source_code or 'type ' in source_code) def analyze_repository_style(self, repo_path: str) -> Dict: """Analyze coding style patterns across repository""" @@ -263,16 +263,15 @@ def analyze_repository_style(self, repo_path: str) -> Dict: } - # ===== SUPABASE CACHING ===== - - def save_to_cache(self, repo_id: str, style_data: Dict): + # SUPABASE CACHING + def save_to_cache(self, repo_id: str, style_data: Dict) -> None: """Save style analysis to Supabase for caching""" from services.supabase_service import get_supabase_service db = get_supabase_service() - # Group by language - for language, data in style_data.get("languages", {}).items(): + # analyze_repository_style returns "language_distribution" not "languages" + for language, data in style_data.get("language_distribution", {}).items(): analysis = { "naming_convention": data.get("naming_conventions"), "async_usage": data.get("async_usage"), @@ -285,7 +284,7 @@ def save_to_cache(self, repo_id: str, style_data: Dict): logger.debug("Cached code style analysis in Supabase", repo_id=repo_id) - def load_from_cache(self, repo_id: str) -> Dict: + def load_from_cache(self, repo_id: str) -> Optional[Dict]: """Load style analysis from Supabase cache""" from services.supabase_service import get_supabase_service @@ -319,7 +318,7 @@ def load_from_cache(self, repo_id: str) -> Dict: logger.debug("Loaded cached code style from Supabase", repo_id=repo_id) return { - "languages": languages, + "language_distribution": languages, "top_imports": common_imports, "patterns": patterns } diff --git a/backend/services/supabase_service.py b/backend/services/supabase_service.py index abaff71..0a8a40d 100644 --- a/backend/services/supabase_service.py +++ b/backend/services/supabase_service.py @@ -6,13 +6,10 @@ from typing import Dict, List, Optional, Any from datetime import datetime from supabase import create_client, Client, ClientOptions -from dotenv import load_dotenv import uuid from services.observability import logger -load_dotenv() - class SupabaseService: """Service for Supabase database operations""" @@ -33,8 +30,7 @@ def __init__(self): self.client: Client = create_client(supabase_url, supabase_key, options) logger.info("Supabase service initialized") - # ===== REPOSITORIES ===== - + # REPOSITORIES def create_repository( self, name: str, @@ -126,8 +122,7 @@ def delete_repository(self, repo_id: str) -> None: """Delete repository (cascades to related tables)""" self.client.table("repositories").delete().eq("id", repo_id).execute() - # ===== FILE DEPENDENCIES ===== - + # FILE DEPENDENCIES def upsert_file_dependencies(self, repo_id: str, dependencies: List[Dict]) -> None: """Bulk upsert file dependencies""" if not dependencies: @@ -158,8 +153,7 @@ def clear_file_dependencies(self, repo_id: str) -> None: """Clear all file dependencies for a repo (for reindexing)""" self.client.table("file_dependencies").delete().eq("repo_id", repo_id).execute() - # ===== CODE STYLE ANALYSIS ===== - + # CODE STYLE ANALYSIS def upsert_code_style(self, repo_id: str, language: str, analysis: Dict) -> None: """Store code style analysis results""" data = { @@ -182,8 +176,7 @@ def get_code_style(self, repo_id: str) -> List[Dict]: result = self.client.table("code_style_analysis").select("*").eq("repo_id", repo_id).execute() return result.data or [] - # ===== REPOSITORY INSIGHTS ===== - + # REPOSITORY INSIGHTS def upsert_repository_insights(self, repo_id: str, insights: Dict) -> None: """Store repository insights""" data = { @@ -206,8 +199,7 @@ def get_repository_insights(self, repo_id: str) -> Optional[Dict]: result = self.client.table("repository_insights").select("*").eq("repo_id", repo_id).execute() return result.data[0] if result.data else None - # ===== INDEXING JOBS ===== - + # INDEXING JOBS def create_indexing_job(self, repo_id: str, total_files: int) -> str: """Create a new indexing job""" data = { diff --git a/backend/services/user_limits.py b/backend/services/user_limits.py index 61a2404..8075c13 100644 --- a/backend/services/user_limits.py +++ b/backend/services/user_limits.py @@ -139,8 +139,7 @@ def _validate_user_id(self, user_id: str) -> bool: return False return True - # ===== TIER MANAGEMENT ===== - + # TIER MANAGEMENT def get_user_tier(self, user_id: str) -> UserTier: """ Get user's current tier. @@ -211,8 +210,7 @@ def invalidate_tier_cache(self, user_id: str) -> None: except Exception as e: logger.warning("Failed to invalidate tier cache", error=str(e)) - # ===== REPO COUNT LIMITS (#95) ===== - + # REPO COUNT LIMITS (#95) def get_user_repo_count(self, user_id: str, raise_on_error: bool = False) -> int: """ Get current repo count for user. @@ -301,8 +299,7 @@ def check_repo_count(self, user_id: str) -> LimitCheckResult: tier=tier.value ) - # ===== REPO SIZE LIMITS (#94) ===== - + # REPO SIZE LIMITS (#94) def check_repo_size( self, user_id: str, @@ -377,14 +374,12 @@ def check_repo_size( tier=tier.value ) - # ===== PLAYGROUND RATE LIMITS (#93) ===== - + # PLAYGROUND RATE LIMITS (#93) def get_playground_limit(self, tier: UserTier = UserTier.FREE) -> Optional[int]: """Get playground search limit for tier""" return self.get_limits(tier).playground_searches_per_day - # ===== USAGE SUMMARY ===== - + # USAGE SUMMARY def get_usage_summary(self, user_id: str) -> Dict[str, Any]: """ Get complete usage summary for user. diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index dddc112..84ca3f2 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -20,9 +20,7 @@ os.environ["SUPABASE_ANON_KEY"] = "test-anon-key" os.environ["SUPABASE_JWT_SECRET"] = "test-jwt-secret" -# ============================================================================= # EARLY PATCHING - runs during collection, before any imports -# ============================================================================= # These patches prevent external service initialization during test collection _pinecone_patcher = patch('pinecone.Pinecone') @@ -47,7 +45,6 @@ _supabase_client.auth.get_user.return_value = _auth_response _mock_supabase.return_value = _supabase_client -# ============================================================================= # Add backend to path backend_dir = Path(__file__).parent.parent diff --git a/backend/tests/test_anonymous_indexing.py b/backend/tests/test_anonymous_indexing.py index 7d0ea47..e2160c5 100644 --- a/backend/tests/test_anonymous_indexing.py +++ b/backend/tests/test_anonymous_indexing.py @@ -22,9 +22,7 @@ ) -# ============================================================================= # REQUEST MODEL TESTS -# ============================================================================= class TestIndexRepoRequest: """Tests for IndexRepoRequest validation.""" @@ -76,9 +74,7 @@ def test_url_whitespace_trimmed(self): assert req.github_url == "https://github.com/user/repo" -# ============================================================================= # JOB MANAGER TESTS -# ============================================================================= class TestAnonymousIndexingJob: """Tests for AnonymousIndexingJob service.""" @@ -230,9 +226,7 @@ def test_update_status_failed_with_error(self, job_manager, mock_redis): assert result is True -# ============================================================================= # JOB DATACLASS TESTS -# ============================================================================= class TestJobDataclasses: """Tests for JobProgress and JobStats.""" @@ -268,9 +262,7 @@ def test_job_stats_to_dict(self): assert d["indexing_time_seconds"] == 45.5 -# ============================================================================= # ENDPOINT TESTS (Integration) -# ============================================================================= class TestIndexEndpoint: """Integration tests for POST /playground/index.""" @@ -428,9 +420,7 @@ def test_github_rate_limit_returns_429(self, mock_metadata, client): assert response.status_code == 429 -# ============================================================================= # SESSION CONFLICT TESTS -# ============================================================================= class TestSessionConflict: """Tests for session-already-has-repo behavior.""" @@ -514,9 +504,7 @@ def test_expired_repo_allows_new_indexing( assert response.json()["job_id"] == "idx_new123456" -# ============================================================================= # STATUS ENDPOINT TESTS (GET /playground/index/{job_id}) -# ============================================================================= class TestStatusEndpoint: """Tests for GET /playground/index/{job_id} status endpoint.""" @@ -682,9 +670,7 @@ def test_partial_job_includes_partial_info(self, mock_job_class, client): -# ============================================================================= # Issue #128: Search User-Indexed Repos Tests -# ============================================================================= class TestSearchUserRepos: """Tests for searching user-indexed repositories.""" diff --git a/backend/tests/test_multi_tenancy.py b/backend/tests/test_multi_tenancy.py index 17f2693..e5c334f 100644 --- a/backend/tests/test_multi_tenancy.py +++ b/backend/tests/test_multi_tenancy.py @@ -30,8 +30,7 @@ sys.path.insert(0, str(backend_dir)) -# ============== TEST DATA ============== - +# TEST DATA REPOS_DB = [ { "id": "repo-user1-a", "name": "User1 Repo A", "user_id": "user-1", @@ -52,8 +51,7 @@ ] -# ============== UNIT TESTS FOR SUPABASE SERVICE ============== - +# UNIT TESTS FOR SUPABASE SERVICE class TestSupabaseServiceOwnership: """Unit tests for ownership verification methods in SupabaseService""" @@ -121,8 +119,7 @@ def test_verify_repo_ownership_returns_true_for_owner(self): assert sig.return_annotation == bool -# ============== UNIT TESTS FOR REPO MANAGER ============== - +# UNIT TESTS FOR REPO MANAGER class TestRepoManagerOwnership: """Unit tests for ownership methods in RepoManager""" @@ -163,8 +160,7 @@ def test_verify_ownership_delegates_to_supabase(self): assert result is False -# ============== HELPER FUNCTION TESTS ============== - +# HELPER FUNCTION TESTS class TestSecurityHelpers: """Test the get_repo_or_404 and verify_repo_access helpers""" @@ -200,14 +196,8 @@ def test_verify_repo_access_raises_404_for_wrong_user(self): assert exc_info.value.status_code == 404 - with pytest.raises(HTTPException) as exc_info: - verify_repo_access("repo-user2-a", "user-1") - - assert exc_info.value.status_code == 404 - - -# ============== DEV API KEY TESTS ============== +# DEV API KEY TESTS class TestDevApiKeySecurity: """Test that dev API key is properly secured (Issue #8)""" @@ -294,8 +284,7 @@ def test_wrong_dev_key_fails_even_in_debug(self): self._reload_auth_module() # Reload to known good state -# ============== INFO LEAKAGE TESTS ============== - +# INFO LEAKAGE TESTS class TestInfoLeakagePrevention: """Test that 404 is returned instead of 403 to prevent info leakage""" @@ -318,8 +307,7 @@ def test_nonexistent_and_unauthorized_get_same_error(self): assert exc1.value.detail == exc2.value.detail -# ============== INTEGRATION-STYLE TESTS ============== - +# INTEGRATION-STYLE TESTS class TestEndpointOwnershipIntegration: """ These tests verify that endpoints actually call ownership verification. diff --git a/backend/tests/test_playground_limiter.py b/backend/tests/test_playground_limiter.py index 08d4b6a..b1339f7 100644 --- a/backend/tests/test_playground_limiter.py +++ b/backend/tests/test_playground_limiter.py @@ -20,9 +20,7 @@ ) -# ============================================================================= # FIXTURES -# ============================================================================= @pytest.fixture def mock_redis(): @@ -71,9 +69,7 @@ def sample_indexed_repo(): } -# ============================================================================= # DATA CLASS TESTS -# ============================================================================= class TestIndexedRepoData: """Tests for IndexedRepoData dataclass.""" @@ -159,9 +155,7 @@ def test_truncate_id(self): assert SessionData._truncate_id("verylongsessiontoken123") == "verylong..." -# ============================================================================= # GET SESSION DATA TESTS -# ============================================================================= class TestGetSessionData: """Tests for get_session_data() method.""" @@ -228,9 +222,7 @@ def test_invalid_indexed_repo_json(self, limiter, mock_redis): assert result.searches_used == 5 -# ============================================================================= # SET INDEXED REPO TESTS -# ============================================================================= class TestSetIndexedRepo: """Tests for set_indexed_repo() method.""" @@ -270,9 +262,7 @@ def test_preserves_other_fields(self, limiter, mock_redis, sample_indexed_repo): assert not mock_redis.set.called -# ============================================================================= # HAS INDEXED REPO TESTS -# ============================================================================= class TestHasIndexedRepo: """Tests for has_indexed_repo() method.""" @@ -303,9 +293,7 @@ def test_repo_not_exists(self, limiter, mock_redis): assert limiter.has_indexed_repo("token") is False -# ============================================================================= # CLEAR INDEXED REPO TESTS -# ============================================================================= class TestClearIndexedRepo: """Tests for clear_indexed_repo() method.""" @@ -325,9 +313,7 @@ def test_no_token_returns_false(self, limiter): assert limiter.clear_indexed_repo(None) is False -# ============================================================================= # LEGACY MIGRATION TESTS -# ============================================================================= class TestLegacyMigration: """Tests for legacy string format migration.""" @@ -366,9 +352,7 @@ def test_nonexistent_key_no_error(self, limiter, mock_redis): limiter._ensure_hash_format("new_token") -# ============================================================================= # RATE LIMITING WITH HASH STORAGE TESTS -# ============================================================================= class TestRateLimitingWithHash: """Tests to verify rate limiting still works with hash storage.""" @@ -404,9 +388,7 @@ def test_new_session_creates_hash(self, limiter, mock_redis): mock_redis.hset.assert_called() -# ============================================================================= # CREATE SESSION TESTS -# ============================================================================= class TestCreateSession: """Tests for create_session() method.""" @@ -432,9 +414,7 @@ def test_no_redis_returns_false(self, limiter_no_redis): assert limiter_no_redis.create_session("token") is False -# ============================================================================= # HELPER METHOD TESTS -# ============================================================================= class TestHelperMethods: """Tests for helper methods.""" @@ -462,9 +442,7 @@ def test_generate_session_token(self, limiter): assert len(token1) > 20 # Should be reasonably long -# ============================================================================= # INTEGRATION-STYLE TESTS -# ============================================================================= class TestSessionWorkflow: """End-to-end workflow tests.""" diff --git a/backend/tests/test_validate_repo.py b/backend/tests/test_validate_repo.py index 03bd027..59df0a9 100644 --- a/backend/tests/test_validate_repo.py +++ b/backend/tests/test_validate_repo.py @@ -16,9 +16,7 @@ ) -# ============================================================================= # URL PARSING TESTS -# ============================================================================= class TestParseGitHubUrl: """Tests for URL parsing.""" @@ -89,9 +87,7 @@ def test_pattern_rejects_subpath(self): assert match is None -# ============================================================================= # REQUEST MODEL TESTS -# ============================================================================= class TestValidateRepoRequest: """Tests for the request model validation.""" @@ -128,9 +124,7 @@ def test_url_with_whitespace_trimmed(self): assert req.github_url == "https://github.com/user/repo" -# ============================================================================= # GITHUB API TESTS -# ============================================================================= class TestFetchRepoMetadata: """Tests for GitHub API interaction.""" @@ -217,9 +211,7 @@ async def test_timeout_handling(self): assert result["error"] == "timeout" -# ============================================================================= # FILE COUNTING TESTS -# ============================================================================= class TestCountCodeFiles: """Tests for file counting logic.""" @@ -359,9 +351,7 @@ async def test_skip_git_directory(self): assert error is None -# ============================================================================= # CONSTANTS TESTS -# ============================================================================= class TestAnonymousFileLimit: """Tests for file limit constant."""