Skip to content

Commit 3c22e8d

Browse files
committed
Merge branch 'tests' into dev
2 parents 5f28397 + c42bec3 commit 3c22e8d

File tree

19 files changed

+2531
-112
lines changed

19 files changed

+2531
-112
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,8 @@ After running this command, you'll need to either:
328328
This often helps to run supabase.
329329

330330
Then, to work with it locally, you can access the local instance's info with `supabase status` and use those to manage it (and e.g. set .env vars).
331-
The **Studio URL** gives you a graphical interface to supabase, and with **Inbucket URL** is a local email smtp server where you can test email signup.
331+
The **Studio URL** gives you a graphical interface to supabase, and with **Inbucket URL** is a local email smtp server where you can test email signup.
332+
333+
## Testing
334+
335+
For detailed testing instructions, see [TESTING.md](TESTING.md).

TESTING.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Testing Guide for SimScore API
2+
3+
## Overview
4+
5+
This guide describes the testing approach for SimScore API, covering test organization, setup instructions, and common testing scenarios.
6+
7+
## Test Organization
8+
9+
```
10+
tests/
11+
├── api/ # API endpoint tests
12+
│ └── v1/
13+
│ └── routes/
14+
│ ├── test_ideas.py # Idea ranking endpoint tests
15+
│ ├── test_auth.py # Authentication endpoint tests
16+
│ └── test_rate_limit.py # Rate limiting tests
17+
├── integration/ # End-to-end flows
18+
│ ├── test_auth_basic.py # Basic authentication flows
19+
│ ├── test_auth_guest.py # Guest user flows
20+
│ └── test_auth_verified.py # Verified user flows
21+
└── conftest.py # Shared test fixtures
22+
```
23+
24+
## Test Environment Setup
25+
26+
1. Create a `.env` file with test configuration:
27+
28+
```
29+
# Environment
30+
ENVIRONMENT=DEV
31+
32+
# API Configuration
33+
RATE_LIMIT_PER_USER=20/minute
34+
GLOBAL_RATE_LIMIT=1000/minute
35+
36+
# Test Settings
37+
SKIP_EMAIL_VERIFICATION=true
38+
TEST_API_TOKEN=<your-test-token>
39+
40+
# Database (Local Supabase)
41+
DATABASE_URL=http://127.0.0.1:54321
42+
DATABASE_KEY=<your-supabase-service-role-key>
43+
44+
# Credits Configuration
45+
GUEST_DAILY_CREDITS=10
46+
GUEST_MAX_CREDITS=100
47+
USER_DAILY_CREDITS=100
48+
USER_MAX_CREDITS=1000
49+
```
50+
51+
2. Start the API server in development mode:
52+
53+
```bash
54+
poetry run uvicorn app.main:app --reload
55+
```
56+
57+
3. Ensure Supabase is running locally:
58+
59+
```bash
60+
supabase start
61+
```
62+
63+
## Running Tests
64+
65+
### API Endpoint Tests
66+
67+
```bash
68+
# Run all API tests
69+
poetry run pytest tests/api/
70+
71+
# Run specific API tests
72+
poetry run pytest tests/api/v1/routes/test_ideas.py
73+
poetry run pytest tests/api/v1/routes/test_auth.py
74+
poetry run pytest tests/api/v1/routes/test_rate_limit.py
75+
```
76+
77+
### Integration Tests
78+
79+
```bash
80+
# Run all integration tests
81+
poetry run pytest tests/integration/
82+
83+
# Run specific integration flows
84+
poetry run pytest tests/integration/test_auth_basic.py
85+
poetry run pytest tests/integration/test_auth_guest.py
86+
poetry run pytest tests/integration/test_auth_verified.py
87+
```
88+
89+
### Running Tests by Marker
90+
91+
```bash
92+
# Auth-related tests
93+
poetry run pytest -m verified
94+
poetry run pytest -m guest
95+
poetry run pytest -m integration
96+
97+
# Rate limiting tests
98+
poetry run pytest -m rate_limited
99+
```
100+
101+
## Test Categories
102+
103+
### Ideas API Tests
104+
105+
Tests the idea ranking endpoint functionality:
106+
107+
- Input validation (minimum/maximum ideas, size limits)
108+
- Cluster analysis and visualization
109+
- Advanced features (relationship graphs, cluster naming)
110+
- Credit system integration
111+
- Error handling
112+
113+
### Authentication Tests
114+
115+
Tests user management and authentication:
116+
117+
- User registration and login
118+
- Email verification
119+
- API key creation and management
120+
- Rate limiting on auth endpoints
121+
- User credits and quotas
122+
123+
### Rate Limiting Tests
124+
125+
Verifies API protection against abuse:
126+
127+
- Request limiting based on IP address
128+
- Appropriate rate limit configuration
129+
- 429 response handling
130+
131+
## Key Test Fixtures
132+
133+
The `conftest.py` file provides shared fixtures:
134+
135+
- `client`: HTTP client for API requests
136+
- `test_user`: Dynamically generated test credentials
137+
- `auth_headers`: Pre-authenticated request headers
138+
- `mock_ideas`: Sample idea data for testing
139+
- `mock_credit_service`: Credit system bypass
140+
- `disable_limiter`: Disables rate limiting during tests
141+
142+
## Troubleshooting
143+
144+
### Rate Limit Errors
145+
146+
If tests fail with 429 status codes:
147+
148+
```bash
149+
# Option 1: Wait for rate limit reset
150+
sleep 60
151+
152+
# Option 2: Run with disabled rate limiting
153+
DISABLE_RATE_LIMITS=true poetry run pytest
154+
```
155+
156+
### Database Connection Issues
157+
158+
If tests fail to connect to Supabase:
159+
160+
```bash
161+
# Check if Supabase is running
162+
supabase status
163+
164+
# Restart Supabase if needed
165+
supabase stop
166+
supabase start
167+
```
168+
169+
### Authentication Failures
170+
171+
For auth test failures:
172+
173+
```bash
174+
# Ensure email verification is disabled for tests
175+
SKIP_EMAIL_VERIFICATION=true
176+
177+
# Use a pre-configured test token
178+
TEST_API_TOKEN=<valid-test-token>
179+
```
180+
181+
## CI/CD Integration
182+
183+
Tests run automatically on:
184+
- Pull requests to main branch
185+
- Nightly builds
186+
187+
The CI pipeline runs tests in a containerized environment with:
188+
- Isolated test database
189+
- Test-specific rate limits
190+
- Email verification disabled

app/api/v1/routes/auth.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from fastapi import APIRouter, Depends, HTTPException
1+
from fastapi import APIRouter, Depends, HTTPException, Request
22
from pydantic import BaseModel, EmailStr
33
from typing import List
44
import app.core.security as backend
5+
from app.core.limiter import limiter # Import from your limiter module
6+
from app.core.config import settings
57

68
# Request/Response Models
79
class UserCredentials(BaseModel):
@@ -30,12 +32,16 @@ class EmailVerification(BaseModel):
3032

3133
router = APIRouter(tags=["auth"])
3234

35+
print(f"Router prefix: {router.prefix}") # See what prefix is being used
36+
3337
@router.post("/auth/sign_up", response_model=SignupResponse)
34-
async def signup(credentials: UserCredentials) -> SignupResponse:
38+
@limiter.limit("5/minute") # Use slowapi limiter
39+
async def signup(request: Request, credentials: UserCredentials) -> SignupResponse:
3540
"""
3641
Register a new user account.
3742
3843
Args:
44+
request: FastAPI request object
3945
credentials: User email and password
4046
4147
Returns:
@@ -51,6 +57,7 @@ async def signup(credentials: UserCredentials) -> SignupResponse:
5157
email=credentials.email
5258
)
5359
except Exception as e:
60+
print("Failed: ", str(e))
5461
if isinstance(e, HTTPException):
5562
raise e
5663
raise HTTPException(status_code=400, detail=str(e))
@@ -69,7 +76,7 @@ async def verify_email(verification: EmailVerification) -> MessageResponse:
6976
Raises:
7077
HTTPException: 400 if verification fails
7178
"""
72-
try:
79+
try:
7380
await backend.verify_email_code(verification.email, verification.code)
7481
return MessageResponse(message="Email successfully verified")
7582
except Exception as e:
@@ -93,13 +100,40 @@ async def create_api_key(credentials: UserCredentials) -> ApiKeyResponse:
93100
HTTPException: 400 if creation fails, 401 if authentication fails
94101
"""
95102
try:
103+
print("\nCreate API key endpoint hit!") # See if we reach this endpoint
104+
print(f"Creating API key for {credentials.email}")
105+
print("\nCreating API key...")
106+
print(f"Test environment: {settings.ENVIRONMENT == 'DEV'}; SKIP EMAIL VERIFICATION: {settings.SKIP_EMAIL_VERIFICATION}")
107+
log = f"Authenticating user {credentials.email} with password {credentials.password}"
96108
user = await backend.authenticate_user(credentials.email, credentials.password)
109+
log = f"User authenticated: {user}"
110+
print(f"User authenticated: {user}")
111+
print(f"User metadata: {user.user_metadata}")
112+
113+
# Skip verification check in test environment
114+
if not (settings.ENVIRONMENT == "TEST" and settings.SKIP_EMAIL_VERIFICATION):
115+
log = "Checking if email is verified"
116+
# Check if email is verified (only in non-test environment)
117+
if not user.user_metadata["email_verified"]:
118+
log = "Email not verified"
119+
raise HTTPException(
120+
status_code=403,
121+
detail="Email not verified. Please verify your email before creating API keys."
122+
)
123+
else:
124+
log = "Test environment - skipping email verification check"
125+
print("Test environment - skipping email verification check")
126+
127+
log = "Creating API key"
97128
api_key = backend.create_api_key(user)
129+
log = f"API key created: {api_key}"
130+
print(f"API key created: {api_key}")
98131
return ApiKeyResponse(api_key=api_key)
99132
except Exception as e:
133+
print(f"API key creation error: {str(e)}")
100134
if isinstance(e, HTTPException):
101135
raise e
102-
raise HTTPException(status_code=400, detail=str(e))
136+
raise HTTPException(status_code=400, detail=log)
103137

104138
@router.delete("/auth/revoke_api_key/{key}", response_model=MessageResponse)
105139
async def delete_api_key(

app/api/v1/routes/ideas.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
router = APIRouter(tags=["ideas"])
1717

1818
@router.post("/rank_ideas", response_model=AnalysisResponse)
19-
@limiter.limit(settings.RATE_LIMIT_PER_USER)
19+
@limiter.limit(
20+
settings.RATE_LIMIT_PER_USER,
21+
key_func=lambda request: request.client.host if request.client else "global"
22+
)
2023
async def rank_ideas(
2124
request: Request,
2225
ideaRequest: IdeaRequest,

app/core/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pydantic_settings import SettingsConfigDict, BaseSettings
22
from typing import Dict, TypedDict
3+
import os
34

45
class OperationCost(TypedDict):
56
base_cost: float
@@ -50,6 +51,14 @@ class Settings(BaseSettings):
5051
USER_DAILY_CREDITS: int
5152
USER_MAX_CREDITS: int
5253

54+
# Environment
55+
ENVIRONMENT: str = "DEV"
56+
57+
# Test Configuration
58+
SKIP_EMAIL_VERIFICATION: bool = False
59+
60+
TEST_API_TOKEN: str = os.getenv("TEST_API_TOKEN", "test-api-token-for-unit-tests")
61+
5362
model_config = SettingsConfigDict(env_file=".env")
5463

5564
settings = Settings()

app/core/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
db: Client = create_client(
55
settings.DATABASE_URL,
66
settings.DATABASE_KEY
7-
)
7+
)

app/core/limiter.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
from app.core.config import settings
1313

1414
def get_identifier(request):
15-
"""Get unique identifier for rate limiting based on auth status"""
16-
if "authorization" in request.headers:
17-
return request.headers["authorization"]
18-
return get_remote_address(request)
15+
"""Get unique identifier for rate limiting based on auth status"""
16+
if "authorization" in request.headers:
17+
return request.headers["authorization"]
18+
return get_remote_address(request)
1919

2020
limiter = Limiter(
21-
key_func=get_identifier,
22-
default_limits=[settings.RATE_LIMIT_PER_USER],
23-
strategy="fixed-window",
24-
storage_uri="memory://", # if we have auto-scaling / multiple servers we'd want to change this to a database
21+
key_func=get_identifier,
22+
default_limits=[settings.RATE_LIMIT_PER_USER],
23+
strategy="fixed-window",
24+
storage_uri="memory://" # Use memory storage for testing
2525
)

0 commit comments

Comments
 (0)