Skip to content

Commit b335174

Browse files
committed
Add & fix tests, add cascading user deletion
1 parent 247ed07 commit b335174

File tree

11 files changed

+235
-50
lines changed

11 files changed

+235
-50
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ helpers/.nltk_resources_cache
88
.vscode/settings.json
99
.python-version
1010
app/services/.nltk_resources_cache
11+
.cursor
12+
api_key.py
13+
supabase/*.sql

.vscode/launch.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010
"request": "launch",
1111
"module": "uvicorn",
1212
"args": [
13-
"app:app",
14-
"--reload"
13+
"main:app",
14+
"--reload",
15+
"--host", "0.0.0.0",
16+
"--port", "8000"
1517
],
16-
"jinja": true
18+
"jinja": true,
19+
"env": {
20+
"ENVIRONMENT": "DEV",
21+
"SKIP_EMAIL_VERIFICATION": "true",
22+
},
23+
"justMyCode": true // Set to true if you only want to debug your code, not libraries
1724
}
1825
]
1926
}

app/api/v1/routes/auth.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@ async def signup(request: Request, credentials: UserCredentials) -> SignupRespon
5151
HTTPException: 400 if registration fails
5252
"""
5353
try:
54+
print(f"Signing up user: {credentials.email}")
5455
await backend.create_user(credentials.email, credentials.password)
5556
return SignupResponse(
5657
message="Registration successful. Please check your email to verify your account.",
57-
email=credentials.email
58+
email=credentials.email,
5859
)
5960
except Exception as e:
6061
print("Failed: ", str(e))
@@ -128,6 +129,7 @@ async def create_api_key(credentials: UserCredentials) -> ApiKeyResponse:
128129
return ApiKeyResponse(api_key=api_key)
129130
except Exception as e:
130131
print(f"API key creation error: {str(e)}")
132+
print(f"last log line: {log}")
131133
if isinstance(e, HTTPException):
132134
raise e
133135
raise HTTPException(status_code=400, detail=log)
@@ -204,3 +206,37 @@ async def get_credits(current_user: dict = Depends(backend.verify_token)) -> Cre
204206
if isinstance(e, HTTPException):
205207
raise e
206208
raise HTTPException(status_code=400, detail=str(e))
209+
210+
211+
@router.post("/auth/remove_user", response_model=MessageResponse)
212+
async def remove_user(credentials: UserCredentials) -> MessageResponse:
213+
"""
214+
Remove a user account and all associated data.
215+
216+
Args:
217+
credentials: User email and password
218+
219+
Returns:
220+
Confirmation message
221+
222+
Raises:
223+
HTTPException: 400 if removal fails, 401 if authentication fails
224+
"""
225+
try:
226+
print(f"Authenticating user for removal: {credentials.email}")
227+
# First authenticate the user to ensure they have permission to delete
228+
user = await backend.authenticate_user(credentials.email, credentials.password)
229+
230+
# Get user ID for deletion operations
231+
user_id = user.id
232+
233+
print(f"Deleting user account: {user_id}")
234+
# Delete the user from Supabase auth - cascading constraints will handle related data
235+
await backend.delete_user(user_id)
236+
237+
return MessageResponse(message="User account and all associated data successfully removed")
238+
except Exception as e:
239+
print(f"User removal error: {str(e)}")
240+
if isinstance(e, HTTPException):
241+
raise e
242+
raise HTTPException(status_code=400, detail=str(e))

app/core/config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,18 @@ class Settings(BaseSettings):
4848
}
4949
}
5050

51-
GUEST_DAILY_CREDITS: int
52-
GUEST_MAX_CREDITS: int
53-
USER_DAILY_CREDITS: int
54-
USER_MAX_CREDITS: int
51+
GUEST_DAILY_CREDITS: int = 10
52+
GUEST_MAX_CREDITS: int = 100
53+
USER_DAILY_CREDITS: int = 100
54+
USER_MAX_CREDITS: int = 1000
5555

5656
# Environment
5757
ENVIRONMENT: str = "DEV"
5858

5959
# Test Configuration
6060
SKIP_EMAIL_VERIFICATION: bool = False
61-
61+
# Used in supabase config.toml for testing
62+
REQUIRE_EMAIL_VERIFICATION: bool = False
6263
TEST_API_TOKEN: str = os.getenv("TEST_API_TOKEN", "test-api-token-for-unit-tests")
6364

6465
model_config = SettingsConfigDict(env_file=".env")

app/core/security.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ async def authenticate_user(email: str, password: str):
2323
session = db.auth.sign_in_with_password({
2424
"email": email,
2525
"password": password
26-
})
26+
})
2727
except Exception as e:
28-
raise HTTPException(status_code=401, detail="This user or password does not exist.")
28+
raise HTTPException(status_code=401, detail=f"This user or password does not exist. {str(e)}")
2929

3030
print("Auth'ed")
3131
user = session.user
@@ -130,14 +130,9 @@ async def verify_token(request: Request, credentials: Optional[HTTPAuthorization
130130
try:
131131
# Skip email verification in test environment
132132
if settings.ENVIRONMENT == "TEST" and settings.SKIP_EMAIL_VERIFICATION:
133-
print("Test environment detected - skipping email verification")
133+
print("Test environment detected - skipping email verification and checking database for user details")
134134
is_guest = not credentials
135-
if is_guest:
136-
return generate_guest_id(request)
137-
138-
# Fix: When in test environment, we need to use a safe way to get user_id
139-
# If we have credentials, try to decode them, otherwise use a test value
140-
user_id = "test_user"
135+
user_id = f"test_user{'_guest' if is_guest else ''}"
141136
if credentials:
142137
try:
143138
decoded = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=["HS256"])
@@ -147,9 +142,9 @@ async def verify_token(request: Request, credentials: Optional[HTTPAuthorization
147142

148143
return {
149144
"user_id": user_id,
150-
"is_guest": False,
145+
"is_guest": is_guest,
151146
"email_verified": True, # Always verified in tests
152-
"balance": settings.USER_MAX_CREDITS
147+
"balance": settings.GUEST_MAX_CREDITS if is_guest else settings.USER_MAX_CREDITS
153148
}
154149

155150
# Regular verification logic...
@@ -232,4 +227,23 @@ def generate_guest_id(request: Request) -> dict:
232227
ip = request.client.host
233228
# Hash the IP to get 32 hex chars
234229
hex_hash = hashlib.sha256(ip.encode()).hexdigest()[:32]
235-
return {"id": f"{UUID(hex_hash)}"}
230+
return {"id": f"{UUID(hex_hash)}"}
231+
232+
async def delete_user(user_id: str):
233+
"""Delete a user from Supabase auth system
234+
235+
Args:
236+
user_id: The ID of the user to delete
237+
238+
Raises:
239+
HTTPException: If deletion fails
240+
"""
241+
try:
242+
# Use the Supabase admin auth client to delete the user
243+
db.auth.sign_out()
244+
db.auth.admin.delete_user(user_id)
245+
print(f"User {user_id} successfully deleted")
246+
except Exception as e:
247+
print(f"Error deleting user {user_id}: {str(e)}")
248+
traceback.print_exc()
249+
raise HTTPException(status_code=400, detail=f"Failed to delete user: {str(e)}")

supabase/config.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ max_client_conn = 100
4444

4545
[db.seed]
4646
# If enabled, seeds the database after migrations during a db reset.
47-
enabled = false
47+
enabled = true
4848
# Specifies an ordered list of seed files to load during db reset.
4949
# Supports glob patterns relative to supabase directory: './seeds/*.sql'
5050
sql_paths = ['./seed.sql']
@@ -124,9 +124,9 @@ password_requirements = ""
124124
enable_signup = true
125125
# If enabled, a user will be required to confirm any email change on both the old, and new email
126126
# addresses. If disabled, only the new email is required to confirm.
127-
double_confirm_changes = true
127+
double_confirm_changes = false
128128
# If enabled, users need to confirm their email address before signing in.
129-
enable_confirmations = true
129+
enable_confirmations = "env(REQUIRE_EMAIL_VERIFICATION)"
130130
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
131131
secure_password_change = false
132132
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- For api_keys table
2+
ALTER TABLE public.api_keys
3+
DROP CONSTRAINT api_keys_user_id_fkey,
4+
ADD CONSTRAINT api_keys_user_id_fkey
5+
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
6+
7+
-- For credit_transactions table
8+
ALTER TABLE public.credit_transactions
9+
DROP CONSTRAINT credit_transactions_user_id_fkey,
10+
ADD CONSTRAINT credit_transactions_user_id_fkey
11+
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
12+
13+
-- For credits table
14+
ALTER TABLE public.credits
15+
DROP CONSTRAINT credits_user_id_fkey,
16+
ADD CONSTRAINT credits_user_id_fkey
17+
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;

tests/api/v1/routes/test_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,9 +743,9 @@ def test_get_credits_guest_user(mock_auth_flow, mock_db_query):
743743

744744
# Patch the verify_token function to return a guest user
745745
with patch('app.core.security.verify_token', return_value=mock_user):
746-
response = client.get("/auth/credits", headers={'Authorization': 'Bearer guest_token'})
746+
response = client.get("/auth/credits")
747747
assert response.status_code == 200
748-
assert response.json()["credits"] == settings.USER_MAX_CREDITS
748+
assert response.json()["credits"] == settings.GUEST_MAX_CREDITS
749749

750750
def test_get_credits_rate_limit():
751751
"""Test rate limiting for credits endpoint"""

tests/api/v1/routes/test_rate_limit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def create_api_key(request: Request):
5656
@app.get("/auth/credits")
5757
@limiter.limit("10/minute")
5858
async def get_credits(request: Request):
59-
return {"credits": 1000}
59+
return {"credits": 100}
6060

6161
return TestClient(app)
6262

tests/conftest.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ def pytest_configure(config):
2121
config.inicfg['asyncio_mode'] = 'auto'
2222
config.inicfg['asyncio_default_fixture_loop_scope'] = 'function'
2323

24+
# Always use 'TEST' environment when running tests!
25+
@pytest.fixture(autouse=True)
26+
def test_settings(monkeypatch):
27+
"""Override settings for all tests"""
28+
monkeypatch.setattr("app.core.config.settings.ENVIRONMENT", "TEST")
29+
yield
30+
2431
@pytest.fixture
2532
def mock_user_info():
2633
return {"user_id": "test_user"}
@@ -158,18 +165,26 @@ def _generate_realistic_similarity_matrix(size: int) -> List[List[float]]:
158165

159166
@pytest.fixture
160167
def test_user():
161-
"""Generate unique test user credentials"""
162-
timestamp = int(datetime.now(UTC).timestamp())
163-
return {
164-
"email": f"test_{timestamp}@example.com",
165-
"password": "SecureTestPass123!"
166-
}
168+
"""Generate unique test user credentials with optional identifier"""
169+
def _create_user(identifier=None):
170+
timestamp = int(datetime.now(UTC).timestamp())
171+
email_prefix = "test_"
172+
173+
if identifier:
174+
email_prefix = f"{email_prefix}{identifier}_"
175+
176+
return {
177+
"email": f"{email_prefix}{timestamp}@example.com",
178+
"password": "SecureTestPass123!"
179+
}
180+
return _create_user
181+
167182

168183
@pytest.fixture
169184
def client():
170185
"""Create a test client that connects to the running server"""
171186
import httpx
172-
with httpx.Client(base_url="http://localhost:8000", timeout=30.0) as client:
187+
with httpx.Client(base_url="http://localhost:8000", timeout=50.0) as client:
173188
yield client
174189

175190
@pytest.fixture

0 commit comments

Comments
 (0)