55import pytest
66import sys
77from pathlib import Path
8- from unittest .mock import Mock , MagicMock , patch
8+ from unittest .mock import MagicMock , patch
99import os
1010
1111# Set test environment BEFORE imports
2020os .environ ["SUPABASE_ANON_KEY" ] = "test-anon-key"
2121os .environ ["SUPABASE_JWT_SECRET" ] = "test-jwt-secret"
2222
23+ # =============================================================================
24+ # EARLY PATCHING - runs during collection, before any imports
25+ # =============================================================================
26+ # These patches prevent external service initialization during test collection
27+
28+ _pinecone_patcher = patch ('pinecone.Pinecone' )
29+ _mock_pinecone = _pinecone_patcher .start ()
30+ _pc_instance = MagicMock ()
31+ _pc_instance .list_indexes .return_value .names .return_value = []
32+ _pc_instance .Index .return_value = MagicMock ()
33+ _mock_pinecone .return_value = _pc_instance
34+
35+ _openai_patcher = patch ('openai.AsyncOpenAI' )
36+ _mock_openai = _openai_patcher .start ()
37+ _openai_client = MagicMock ()
38+ _mock_openai .return_value = _openai_client
39+
40+ _supabase_patcher = patch ('supabase.create_client' )
41+ _mock_supabase = _supabase_patcher .start ()
42+ _supabase_client = MagicMock ()
43+ _supabase_client .table .return_value .select .return_value .execute .return_value .data = []
44+ # Auth should reject by default - set user to None
45+ _auth_response = MagicMock ()
46+ _auth_response .user = None
47+ _supabase_client .auth .get_user .return_value = _auth_response
48+ _mock_supabase .return_value = _supabase_client
49+
50+ # =============================================================================
51+
2352# Add backend to path
2453backend_dir = Path (__file__ ).parent .parent
2554sys .path .insert (0 , str (backend_dir ))
@@ -50,29 +79,29 @@ def mock_pinecone():
5079 yield mock
5180
5281
53- @pytest .fixture (scope = "session" , autouse = True )
82+ @pytest .fixture (scope = "session" , autouse = True )
5483def mock_redis ():
5584 """
5685 Mock Redis client globally.
57-
86+
5887 Includes hash operations for session management (#127).
5988 """
6089 with patch ('redis.Redis' ) as mock :
6190 redis_instance = MagicMock ()
62-
91+
6392 # Connection
6493 redis_instance .ping .return_value = True
65-
94+
6695 # String operations (legacy + IP/global limits)
6796 redis_instance .get .return_value = None
6897 redis_instance .set .return_value = True
6998 redis_instance .incr .return_value = 1
7099 redis_instance .delete .return_value = 1
71-
100+
72101 # TTL operations
73102 redis_instance .expire .return_value = True
74103 redis_instance .ttl .return_value = 86400
75-
104+
76105 # Hash operations (session management #127)
77106 redis_instance .type .return_value = b'hash'
78107 redis_instance .hset .return_value = 1
@@ -84,7 +113,7 @@ def mock_redis():
84113 redis_instance .hincrby .return_value = 1
85114 redis_instance .hexists .return_value = False
86115 redis_instance .hdel .return_value = 1
87-
116+
88117 mock .return_value = redis_instance
89118 yield mock
90119
@@ -95,30 +124,30 @@ def mock_supabase():
95124 with patch ('supabase.create_client' ) as mock :
96125 client = MagicMock ()
97126 table = MagicMock ()
98-
127+
99128 # Mock the fluent interface for tables
100129 execute_result = MagicMock ()
101130 execute_result .data = []
102131 execute_result .count = 0
103-
132+
104133 table .select .return_value = table
105134 table .insert .return_value = table
106135 table .update .return_value = table
107- table .delete .return_value = table
136+ table .delete .return_value = table
108137 table .eq .return_value = table
109138 table .order .return_value = table
110139 table .limit .return_value = table
111140 table .upsert .return_value = table
112141 table .execute .return_value = execute_result
113-
142+
114143 client .table .return_value = table
115-
144+
116145 # Mock auth.get_user to reject invalid tokens
117146 # By default, return response with user=None (invalid token)
118147 auth_response = MagicMock ()
119148 auth_response .user = None
120149 client .auth .get_user .return_value = auth_response
121-
150+
122151 mock .return_value = client
123152 yield mock
124153
@@ -141,20 +170,20 @@ def client():
141170 from fastapi .testclient import TestClient
142171 from main import app
143172 from middleware .auth import AuthContext
144-
173+
145174 # Override the require_auth dependency to always return a valid context
146175 async def mock_require_auth ():
147176 return AuthContext (
148177 user_id = "test-user-123" ,
149178 email = "test@example.com" ,
150179 tier = "enterprise"
151180 )
152-
181+
153182 from middleware .auth import require_auth
154183 app .dependency_overrides [require_auth ] = mock_require_auth
155-
184+
156185 yield TestClient (app )
157-
186+
158187 # Cleanup
159188 app .dependency_overrides .clear ()
160189
@@ -169,7 +198,7 @@ def client_no_auth():
169198
170199@pytest .fixture
171200def valid_headers ():
172- """Valid authentication headers (not actually used with mocked auth, but kept for compatibility) """
201+ """Valid auth headers (kept for compatibility with mocked auth). """
173202 return {"Authorization" : "Bearer test-secret-key" }
174203
175204
@@ -182,6 +211,7 @@ def sample_repo_payload():
182211 "branch" : "main"
183212 }
184213
214+
185215@pytest .fixture
186216def malicious_payloads ():
187217 """Collection of malicious inputs for security testing"""
0 commit comments