1116 lines
27 KiB
Markdown
1116 lines
27 KiB
Markdown
# Environment Detection System
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Audience:** Development Team
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [How It Works](#how-it-works)
|
|
3. [Using Environment Detection](#using-environment-detection)
|
|
4. [Configuration](#configuration)
|
|
5. [Development Guidelines](#development-guidelines)
|
|
6. [Deployment Guide](#deployment-guide)
|
|
7. [Testing](#testing)
|
|
8. [Best Practices](#best-practices)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The Wizamart platform uses **automatic environment detection** to determine the runtime environment (development, staging, or production) and adjust security settings accordingly.
|
|
|
|
### Why Auto-Detection?
|
|
|
|
Instead of manually configuring environment settings in code, the system automatically detects the environment based on:
|
|
- Environment variables
|
|
- Runtime indicators (hostname, debug mode, etc.)
|
|
- Safe defaults for local development
|
|
|
|
This approach:
|
|
- ✅ Works out of the box in development (no configuration needed)
|
|
- ✅ Reduces configuration errors
|
|
- ✅ Follows "convention over configuration" principle
|
|
- ✅ Simplifies deployment process
|
|
|
|
### Key Use Cases
|
|
|
|
The environment detection system is primarily used to determine:
|
|
1. **Cookie Security** - Should cookies require HTTPS? (`secure` flag)
|
|
2. **Debug Mode** - Enable detailed error messages in development
|
|
3. **CORS Settings** - Stricter in production, relaxed in development
|
|
4. **Logging Level** - Verbose in development, structured in production
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
### Detection Logic
|
|
|
|
The system determines the environment using a priority-based approach:
|
|
|
|
```
|
|
1. Check ENV environment variable
|
|
↓ (if not set)
|
|
2. Check ENVIRONMENT environment variable
|
|
↓ (if not set)
|
|
3. Auto-detect from system indicators
|
|
↓ (if no indicators)
|
|
4. Default to "development" (safe for local work)
|
|
```
|
|
|
|
### Detection Details
|
|
|
|
#### Priority 1: ENV Variable
|
|
```bash
|
|
ENV=production
|
|
ENV=staging
|
|
ENV=development # or "dev" or "local"
|
|
```
|
|
|
|
#### Priority 2: ENVIRONMENT Variable
|
|
```bash
|
|
ENVIRONMENT=production
|
|
ENVIRONMENT=staging
|
|
ENVIRONMENT=development
|
|
```
|
|
|
|
#### Priority 3: Auto-Detection
|
|
|
|
The system checks for indicators:
|
|
|
|
**Development Indicators:**
|
|
- `DEBUG=true` environment variable
|
|
- Hostname contains: "local", "dev", "laptop", "desktop"
|
|
- Running on: localhost, 127.0.0.1
|
|
|
|
**Staging Indicators:**
|
|
- Hostname contains: "staging", "stage"
|
|
|
|
**Production Indicators:**
|
|
- None of the above (explicit production setting recommended)
|
|
|
|
#### Priority 4: Safe Default
|
|
|
|
If no environment is detected: **defaults to development**
|
|
- Safe for local development
|
|
- Cookies work with HTTP
|
|
- Detailed error messages enabled
|
|
|
|
---
|
|
|
|
## Using Environment Detection
|
|
|
|
### Module Location
|
|
|
|
```python
|
|
app/core/environment.py
|
|
```
|
|
|
|
### Available Functions
|
|
|
|
```python
|
|
from app.core.environment import (
|
|
get_environment, # Returns: "development" | "staging" | "production"
|
|
is_development, # Returns: bool
|
|
is_staging, # Returns: bool
|
|
is_production, # Returns: bool
|
|
should_use_secure_cookies, # Returns: bool
|
|
get_cached_environment, # Returns: cached environment (performance)
|
|
)
|
|
```
|
|
|
|
### Common Usage Patterns
|
|
|
|
#### 1. Cookie Security Settings
|
|
|
|
```python
|
|
from fastapi import Response
|
|
from app.core.environment import should_use_secure_cookies
|
|
|
|
@router.post("/api/v1/admin/auth/login")
|
|
def login(response: Response):
|
|
# Set authentication cookie
|
|
response.set_cookie(
|
|
key="admin_token",
|
|
value=token,
|
|
httponly=True,
|
|
secure=should_use_secure_cookies(), # Auto-detects environment
|
|
samesite="lax",
|
|
path="/admin"
|
|
)
|
|
return {"message": "Logged in"}
|
|
```
|
|
|
|
**Behavior:**
|
|
- **Development** → `secure=False` (works with HTTP)
|
|
- **Production** → `secure=True` (requires HTTPS)
|
|
|
|
#### 2. Conditional Features
|
|
|
|
```python
|
|
from app.core.environment import is_development, is_production
|
|
|
|
@router.get("/api/v1/debug/info")
|
|
def debug_info():
|
|
if not is_development():
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
|
|
# Only accessible in development
|
|
return {
|
|
"database": get_db_info(),
|
|
"cache": get_cache_stats(),
|
|
"config": get_config_dump()
|
|
}
|
|
```
|
|
|
|
#### 3. Error Messages
|
|
|
|
```python
|
|
from app.core.environment import is_development
|
|
|
|
@app.exception_handler(Exception)
|
|
async def exception_handler(request: Request, exc: Exception):
|
|
if is_development():
|
|
# Detailed error in development
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": str(exc),
|
|
"traceback": traceback.format_exc(),
|
|
"request": str(request.url)
|
|
}
|
|
)
|
|
else:
|
|
# Generic error in production
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"error": "Internal server error"}
|
|
)
|
|
```
|
|
|
|
#### 4. CORS Configuration
|
|
|
|
```python
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from app.core.environment import is_development
|
|
|
|
if is_development():
|
|
# Relaxed CORS for development
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
else:
|
|
# Strict CORS for production
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["https://yourdomain.com"],
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
allow_headers=["Content-Type", "Authorization"],
|
|
)
|
|
```
|
|
|
|
#### 5. Logging Configuration
|
|
|
|
```python
|
|
import logging
|
|
from app.core.environment import get_environment
|
|
|
|
def configure_logging():
|
|
env = get_environment()
|
|
|
|
if env == "development":
|
|
# Verbose logging for development
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
elif env == "staging":
|
|
# Moderate logging for staging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
else: # production
|
|
# Structured logging for production
|
|
logging.basicConfig(
|
|
level=logging.WARNING,
|
|
format='{"timestamp":"%(asctime)s","level":"%(levelname)s","message":"%(message)s"}'
|
|
)
|
|
```
|
|
|
|
#### 6. Performance Optimization
|
|
|
|
```python
|
|
from app.core.environment import get_cached_environment
|
|
|
|
def process_request(request: Request):
|
|
# Use cached version for frequent calls
|
|
env = get_cached_environment() # Cached - faster
|
|
|
|
# Instead of:
|
|
# env = get_environment() # Re-detects every time
|
|
|
|
if env == "production":
|
|
# Production-specific optimization
|
|
use_cache = True
|
|
else:
|
|
use_cache = False
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Development (Default)
|
|
|
|
**No configuration needed!** The system auto-detects development mode.
|
|
|
|
**Indicators:**
|
|
- Running on localhost or 127.0.0.1
|
|
- No environment variables set
|
|
- DEBUG mode enabled
|
|
|
|
**Settings:**
|
|
- `should_use_secure_cookies()` → `False`
|
|
- Cookies work with HTTP (http://localhost:8000)
|
|
- Detailed error messages
|
|
|
|
### Staging Environment
|
|
|
|
**Set environment variable:**
|
|
|
|
```bash
|
|
# Option 1: ENV
|
|
export ENV=staging
|
|
|
|
# Option 2: ENVIRONMENT
|
|
export ENVIRONMENT=staging
|
|
|
|
# Start application
|
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
```
|
|
|
|
**Or in Docker:**
|
|
```dockerfile
|
|
ENV ENV=staging
|
|
```
|
|
|
|
**Settings:**
|
|
- `should_use_secure_cookies()` → `True`
|
|
- Requires HTTPS
|
|
- Moderate logging
|
|
|
|
### Production Environment
|
|
|
|
**Set environment variable:**
|
|
|
|
```bash
|
|
# Option 1: ENV
|
|
export ENV=production
|
|
|
|
# Option 2: ENVIRONMENT
|
|
export ENVIRONMENT=production
|
|
|
|
# Start application
|
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
```
|
|
|
|
**Or in systemd service:**
|
|
```ini
|
|
[Service]
|
|
Environment="ENV=production"
|
|
ExecStart=/usr/bin/uvicorn main:app --host 0.0.0.0 --port 8000
|
|
```
|
|
|
|
**Or in Docker Compose:**
|
|
```yaml
|
|
services:
|
|
web:
|
|
image: wizamart:latest
|
|
environment:
|
|
- ENV=production
|
|
ports:
|
|
- "8000:8000"
|
|
```
|
|
|
|
**Settings:**
|
|
- `should_use_secure_cookies()` → `True`
|
|
- Requires HTTPS
|
|
- Minimal logging (warnings/errors only)
|
|
- Generic error messages
|
|
|
|
---
|
|
|
|
## Development Guidelines
|
|
|
|
### When Writing New Code
|
|
|
|
#### Use Environment Functions
|
|
|
|
**DO:**
|
|
```python
|
|
from app.core.environment import should_use_secure_cookies, is_development
|
|
|
|
# Use the utility functions
|
|
if is_development():
|
|
enable_debug_toolbar()
|
|
|
|
response.set_cookie(secure=should_use_secure_cookies())
|
|
```
|
|
|
|
**DON'T:**
|
|
```python
|
|
# Don't manually check environment strings
|
|
if os.getenv("ENV") == "production": # ❌ Bad
|
|
...
|
|
|
|
# Don't hardcode environment logic
|
|
response.set_cookie(secure=True) # ❌ Won't work in development
|
|
```
|
|
|
|
#### Keep Environment Logic Centralized
|
|
|
|
If you need custom environment-based behavior:
|
|
|
|
**Option 1: Add to environment.py**
|
|
```python
|
|
# app/core/environment.py
|
|
|
|
def should_enable_caching() -> bool:
|
|
"""Determine if caching should be enabled."""
|
|
return not is_development()
|
|
|
|
def get_max_upload_size() -> int:
|
|
"""Get maximum upload size based on environment."""
|
|
env = get_environment()
|
|
if env == "development":
|
|
return 10 * 1024 * 1024 # 10MB in dev
|
|
elif env == "staging":
|
|
return 50 * 1024 * 1024 # 50MB in staging
|
|
else:
|
|
return 100 * 1024 * 1024 # 100MB in prod
|
|
```
|
|
|
|
**Option 2: Use in your module**
|
|
```python
|
|
from app.core.environment import is_production
|
|
|
|
def get_cache_ttl() -> int:
|
|
"""Get cache TTL based on environment."""
|
|
return 3600 if is_production() else 60 # 1 hour in prod, 1 min in dev
|
|
```
|
|
|
|
### Testing Environment Detection
|
|
|
|
#### Unit Tests
|
|
|
|
```python
|
|
import os
|
|
import pytest
|
|
from app.core.environment import get_environment, should_use_secure_cookies
|
|
|
|
def test_environment_detection_with_env_var():
|
|
"""Test environment detection with ENV variable."""
|
|
os.environ["ENV"] = "production"
|
|
|
|
# Clear cache first
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
|
|
assert get_environment() == "production"
|
|
assert should_use_secure_cookies() == True
|
|
|
|
# Cleanup
|
|
del os.environ["ENV"]
|
|
|
|
def test_environment_defaults_to_development():
|
|
"""Test that environment defaults to development."""
|
|
# Ensure no env vars set
|
|
os.environ.pop("ENV", None)
|
|
os.environ.pop("ENVIRONMENT", None)
|
|
|
|
# Clear cache
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
|
|
assert get_environment() == "development"
|
|
assert should_use_secure_cookies() == False
|
|
```
|
|
|
|
#### Integration Tests
|
|
|
|
```python
|
|
def test_login_sets_secure_cookie_in_production(test_client, monkeypatch):
|
|
"""Test that login sets secure cookie in production."""
|
|
# Mock production environment
|
|
monkeypatch.setenv("ENV", "production")
|
|
|
|
# Clear cache
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
|
|
# Test login
|
|
response = test_client.post("/api/v1/admin/auth/login", json={
|
|
"username": "admin",
|
|
"password": "admin123"
|
|
})
|
|
|
|
# Check cookie has secure flag
|
|
set_cookie_header = response.headers.get("set-cookie")
|
|
assert "Secure" in set_cookie_header
|
|
```
|
|
|
|
### Debugging Environment Detection
|
|
|
|
#### Print Current Environment
|
|
|
|
```python
|
|
# Add to your startup or debug endpoint
|
|
from app.core.environment import get_environment, should_use_secure_cookies
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
env = get_environment()
|
|
secure = should_use_secure_cookies()
|
|
|
|
print(f"🌍 Environment: {env}")
|
|
print(f"🔒 Secure cookies: {secure}")
|
|
print(f"📍 Running on: {os.getenv('HOSTNAME', 'localhost')}")
|
|
```
|
|
|
|
#### Debug Endpoint (Development Only)
|
|
|
|
```python
|
|
from app.core.environment import get_environment, is_development
|
|
import os
|
|
|
|
@router.get("/debug/environment")
|
|
def debug_environment():
|
|
"""Debug endpoint to check environment detection."""
|
|
if not is_development():
|
|
raise HTTPException(status_code=404)
|
|
|
|
return {
|
|
"detected_environment": get_environment(),
|
|
"should_use_secure_cookies": should_use_secure_cookies(),
|
|
"env_var_ENV": os.getenv("ENV"),
|
|
"env_var_ENVIRONMENT": os.getenv("ENVIRONMENT"),
|
|
"env_var_DEBUG": os.getenv("DEBUG"),
|
|
"hostname": os.getenv("HOSTNAME", "unknown"),
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Deployment Guide
|
|
|
|
### Local Development
|
|
|
|
**Setup:**
|
|
```bash
|
|
# Clone repo
|
|
git clone <wizamart-repo>
|
|
cd wizamart-repo
|
|
|
|
# Create virtual environment
|
|
python -m venv venv
|
|
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
|
|
|
# Install dependencies
|
|
pip install -r requirements.txt
|
|
|
|
# Run application (no environment config needed!)
|
|
uvicorn main:app --reload
|
|
```
|
|
|
|
**Access:**
|
|
- Admin: http://localhost:8000/admin
|
|
- Vendor: http://localhost:8000/vendor/{code}
|
|
- Shop: http://localhost:8000/shop
|
|
|
|
**Environment:**
|
|
- Auto-detected as "development"
|
|
- Cookies work with HTTP
|
|
- Debug mode enabled
|
|
|
|
### Staging Deployment
|
|
|
|
**Docker Compose:**
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
web:
|
|
build: .
|
|
environment:
|
|
- ENV=staging
|
|
- DATABASE_URL=postgresql://user:pass@db:5432/wizamart_staging
|
|
ports:
|
|
- "8000:8000"
|
|
depends_on:
|
|
- db
|
|
|
|
db:
|
|
image: postgres:15
|
|
environment:
|
|
- POSTGRES_DB=wizamart_staging
|
|
- POSTGRES_USER=user
|
|
- POSTGRES_PASSWORD=pass
|
|
```
|
|
|
|
**Kubernetes:**
|
|
```yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: wizamart-staging
|
|
spec:
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: web
|
|
image: wizamart:latest
|
|
env:
|
|
- name: ENV
|
|
value: "staging"
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: db-credentials
|
|
key: url
|
|
```
|
|
|
|
**Requirements:**
|
|
- HTTPS enabled (nginx/traefik)
|
|
- SSL certificates configured
|
|
- Environment variable set: `ENV=staging`
|
|
|
|
### Production Deployment
|
|
|
|
**systemd Service:**
|
|
```ini
|
|
[Unit]
|
|
Description=Wizamart Platform
|
|
After=network.target
|
|
|
|
[Service]
|
|
User=wizamart
|
|
WorkingDirectory=/opt/wizamart
|
|
Environment="ENV=production"
|
|
Environment="DATABASE_URL=postgresql://user:pass@localhost/wizamart_prod"
|
|
ExecStart=/opt/wizamart/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
|
Restart=always
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
**Docker:**
|
|
```dockerfile
|
|
FROM python:3.11-slim
|
|
|
|
WORKDIR /app
|
|
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
COPY . .
|
|
|
|
ENV ENV=production
|
|
ENV PYTHONUNBUFFERED=1
|
|
|
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
|
```
|
|
|
|
**Requirements:**
|
|
- ✅ HTTPS enabled and enforced
|
|
- ✅ SSL certificates valid
|
|
- ✅ Environment variable: `ENV=production`
|
|
- ✅ Database backups configured
|
|
- ✅ Monitoring and logging enabled
|
|
- ✅ Security headers configured
|
|
|
|
### Environment Variable Checklist
|
|
|
|
| Environment | ENV Variable | HTTPS Required | Debug Mode | Cookie Secure |
|
|
|-------------|-------------|----------------|------------|---------------|
|
|
| Development | Not set (or "development") | ❌ No | ✅ Yes | ❌ No |
|
|
| Staging | "staging" | ✅ Yes | ⚠️ Limited | ✅ Yes |
|
|
| Production | "production" | ✅ Yes | ❌ No | ✅ Yes |
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Manual Testing
|
|
|
|
#### Test Environment Detection
|
|
|
|
```bash
|
|
# Test development (default)
|
|
python -c "
|
|
from app.core.environment import get_environment, should_use_secure_cookies
|
|
print(f'Environment: {get_environment()}')
|
|
print(f'Secure cookies: {should_use_secure_cookies()}')
|
|
"
|
|
# Expected output:
|
|
# Environment: development
|
|
# Secure cookies: False
|
|
```
|
|
|
|
```bash
|
|
# Test production
|
|
ENV=production python -c "
|
|
from app.core.environment import get_environment, should_use_secure_cookies
|
|
print(f'Environment: {get_environment()}')
|
|
print(f'Secure cookies: {should_use_secure_cookies()}')
|
|
"
|
|
# Expected output:
|
|
# Environment: production
|
|
# Secure cookies: True
|
|
```
|
|
|
|
#### Test Cookie Behavior
|
|
|
|
**Development:**
|
|
```bash
|
|
# Start server (development mode)
|
|
uvicorn main:app --reload
|
|
|
|
# Login and check cookie
|
|
curl -X POST http://localhost:8000/api/v1/admin/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"admin","password":"admin123"}' \
|
|
-v 2>&1 | grep -i "set-cookie"
|
|
|
|
# Should see: Set-Cookie: admin_token=...; HttpOnly; SameSite=Lax; Path=/admin
|
|
# Should NOT see: Secure flag
|
|
```
|
|
|
|
**Production:**
|
|
```bash
|
|
# Start server (production mode)
|
|
ENV=production uvicorn main:app
|
|
|
|
# Login and check cookie
|
|
curl -X POST https://yourdomain.com/api/v1/admin/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"admin","password":"admin123"}' \
|
|
-v 2>&1 | grep -i "set-cookie"
|
|
|
|
# Should see: Set-Cookie: admin_token=...; Secure; HttpOnly; SameSite=Lax; Path=/admin
|
|
# Should see: Secure flag present
|
|
```
|
|
|
|
### Automated Tests
|
|
|
|
#### pytest Examples
|
|
|
|
```python
|
|
# tests/test_environment.py
|
|
|
|
import pytest
|
|
import os
|
|
from app.core.environment import (
|
|
get_environment,
|
|
is_development,
|
|
is_staging,
|
|
is_production,
|
|
should_use_secure_cookies
|
|
)
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_environment_cache():
|
|
"""Clear cached environment before each test."""
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
yield
|
|
env_module._cached_environment = None
|
|
|
|
class TestEnvironmentDetection:
|
|
def test_default_is_development(self):
|
|
"""Test that default environment is development."""
|
|
# Clear any env vars
|
|
os.environ.pop("ENV", None)
|
|
os.environ.pop("ENVIRONMENT", None)
|
|
|
|
assert get_environment() == "development"
|
|
assert is_development() == True
|
|
assert is_production() == False
|
|
assert should_use_secure_cookies() == False
|
|
|
|
def test_env_variable_production(self, monkeypatch):
|
|
"""Test ENV=production detection."""
|
|
monkeypatch.setenv("ENV", "production")
|
|
|
|
assert get_environment() == "production"
|
|
assert is_production() == True
|
|
assert should_use_secure_cookies() == True
|
|
|
|
def test_environment_variable_staging(self, monkeypatch):
|
|
"""Test ENVIRONMENT=staging detection."""
|
|
monkeypatch.setenv("ENVIRONMENT", "staging")
|
|
|
|
assert get_environment() == "staging"
|
|
assert is_staging() == True
|
|
assert should_use_secure_cookies() == True
|
|
|
|
def test_env_takes_priority_over_environment(self, monkeypatch):
|
|
"""Test that ENV variable has priority."""
|
|
monkeypatch.setenv("ENV", "production")
|
|
monkeypatch.setenv("ENVIRONMENT", "staging")
|
|
|
|
# ENV should win
|
|
assert get_environment() == "production"
|
|
|
|
def test_debug_true_is_development(self, monkeypatch):
|
|
"""Test that DEBUG=true results in development."""
|
|
monkeypatch.delenv("ENV", raising=False)
|
|
monkeypatch.delenv("ENVIRONMENT", raising=False)
|
|
monkeypatch.setenv("DEBUG", "true")
|
|
|
|
assert get_environment() == "development"
|
|
|
|
|
|
class TestCookieSecurity:
|
|
def test_secure_cookies_in_production(self, monkeypatch):
|
|
"""Test cookies are secure in production."""
|
|
monkeypatch.setenv("ENV", "production")
|
|
assert should_use_secure_cookies() == True
|
|
|
|
def test_insecure_cookies_in_development(self):
|
|
"""Test cookies are not secure in development."""
|
|
os.environ.pop("ENV", None)
|
|
assert should_use_secure_cookies() == False
|
|
|
|
|
|
# tests/test_authentication_cookies.py
|
|
|
|
def test_login_cookie_matches_environment(test_client, monkeypatch):
|
|
"""Test that login cookie security matches environment."""
|
|
# Test development
|
|
monkeypatch.delenv("ENV", raising=False)
|
|
response = test_client.post("/api/v1/admin/auth/login", json={
|
|
"username": "admin",
|
|
"password": "admin123"
|
|
})
|
|
|
|
set_cookie = response.headers.get("set-cookie", "")
|
|
assert "Secure" not in set_cookie # No Secure in development
|
|
|
|
# Test production
|
|
monkeypatch.setenv("ENV", "production")
|
|
# Clear cache
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
|
|
response = test_client.post("/api/v1/admin/auth/login", json={
|
|
"username": "admin",
|
|
"password": "admin123"
|
|
})
|
|
|
|
set_cookie = response.headers.get("set-cookie", "")
|
|
assert "Secure" in set_cookie # Secure in production
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### DO ✅
|
|
|
|
1. **Use the environment utility functions:**
|
|
```python
|
|
from app.core.environment import should_use_secure_cookies
|
|
response.set_cookie(secure=should_use_secure_cookies())
|
|
```
|
|
|
|
2. **Set ENV in production/staging:**
|
|
```bash
|
|
export ENV=production
|
|
```
|
|
|
|
3. **Use cached version for frequent calls:**
|
|
```python
|
|
from app.core.environment import get_cached_environment
|
|
env = get_cached_environment() # Cached - faster
|
|
```
|
|
|
|
4. **Keep environment logic in environment.py:**
|
|
- Add new environment-dependent functions to `environment.py`
|
|
- Don't scatter environment checks throughout codebase
|
|
|
|
5. **Document environment-dependent behavior:**
|
|
```python
|
|
def send_email(to: str, subject: str):
|
|
"""
|
|
Send email to recipient.
|
|
|
|
Environment behavior:
|
|
- Development: Logs email to console (no actual send)
|
|
- Staging: Sends to test email address
|
|
- Production: Sends to actual recipient
|
|
"""
|
|
if is_development():
|
|
logger.info(f"[DEV] Email to {to}: {subject}")
|
|
elif is_staging():
|
|
actual_to = f"staging+{to}@example.com"
|
|
send_via_smtp(actual_to, subject)
|
|
else:
|
|
send_via_smtp(to, subject)
|
|
```
|
|
|
|
6. **Test with different environments:**
|
|
```python
|
|
@pytest.mark.parametrize("env", ["development", "staging", "production"])
|
|
def test_feature_in_all_environments(env, monkeypatch):
|
|
monkeypatch.setenv("ENV", env)
|
|
# Clear cache
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
# Test your feature
|
|
```
|
|
|
|
### DON'T ❌
|
|
|
|
1. **Don't hardcode environment checks:**
|
|
```python
|
|
# ❌ Bad
|
|
if os.getenv("ENV") == "production":
|
|
use_https = True
|
|
|
|
# ✅ Good
|
|
use_https = should_use_secure_cookies()
|
|
```
|
|
|
|
2. **Don't assume environment:**
|
|
```python
|
|
# ❌ Bad - assumes production
|
|
response.set_cookie(secure=True)
|
|
|
|
# ✅ Good - adapts to environment
|
|
response.set_cookie(secure=should_use_secure_cookies())
|
|
```
|
|
|
|
3. **Don't bypass environment detection:**
|
|
```python
|
|
# ❌ Bad
|
|
FORCE_PRODUCTION = True
|
|
if FORCE_PRODUCTION:
|
|
...
|
|
|
|
# ✅ Good - use environment variable
|
|
# export ENV=production
|
|
if is_production():
|
|
...
|
|
```
|
|
|
|
4. **Don't mix environment checking methods:**
|
|
```python
|
|
# ❌ Bad - inconsistent
|
|
if os.getenv("ENV") == "prod":
|
|
log_level = "WARNING"
|
|
elif is_development():
|
|
log_level = "DEBUG"
|
|
|
|
# ✅ Good - consistent
|
|
env = get_environment()
|
|
if env == "production":
|
|
log_level = "WARNING"
|
|
elif env == "development":
|
|
log_level = "DEBUG"
|
|
```
|
|
|
|
5. **Don't forget to clear cache in tests:**
|
|
```python
|
|
# ❌ Bad - cache may interfere
|
|
def test_production():
|
|
os.environ["ENV"] = "production"
|
|
assert get_environment() == "production" # May fail if cached
|
|
|
|
# ✅ Good - clear cache
|
|
def test_production():
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
os.environ["ENV"] = "production"
|
|
assert get_environment() == "production"
|
|
```
|
|
|
|
### Security Considerations
|
|
|
|
1. **Always use HTTPS in production:**
|
|
- Environment detection enables `secure=True` for cookies
|
|
- But you must configure HTTPS at the server level
|
|
|
|
2. **Never disable security in production:**
|
|
```python
|
|
# ❌ NEVER do this
|
|
if is_production():
|
|
response.set_cookie(secure=False) # Security vulnerability!
|
|
```
|
|
|
|
3. **Validate environment variable values:**
|
|
- The system only accepts: "development", "staging", "production"
|
|
- Invalid values default to "development" (safe)
|
|
|
|
4. **Monitor environment in production:**
|
|
```python
|
|
@app.on_event("startup")
|
|
async def check_production_settings():
|
|
if is_production():
|
|
# Verify production requirements
|
|
assert os.getenv("DATABASE_URL"), "DATABASE_URL required in production"
|
|
assert os.getenv("SECRET_KEY"), "SECRET_KEY required in production"
|
|
logger.info("✅ Production environment verified")
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: Cookies Not Working
|
|
|
|
**Symptom:** Authentication cookies not being sent/received
|
|
|
|
**Check:**
|
|
1. **Environment detection:**
|
|
```python
|
|
from app.core.environment import get_environment, should_use_secure_cookies
|
|
print(f"Environment: {get_environment()}")
|
|
print(f"Secure cookies: {should_use_secure_cookies()}")
|
|
```
|
|
|
|
2. **Development (HTTP):**
|
|
- `should_use_secure_cookies()` should return `False`
|
|
- Access via `http://localhost:8000` (not HTTPS)
|
|
|
|
3. **Production (HTTPS):**
|
|
- `should_use_secure_cookies()` should return `True`
|
|
- Access via `https://yourdomain.com` (with HTTPS)
|
|
- Verify SSL certificate is valid
|
|
|
|
### Issue: Wrong Environment Detected
|
|
|
|
**Symptom:** System detects wrong environment
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Explicitly set environment
|
|
export ENV=production
|
|
|
|
# Restart application
|
|
uvicorn main:app --reload
|
|
|
|
# Verify
|
|
python -c "from app.core.environment import get_environment; print(get_environment())"
|
|
```
|
|
|
|
### Issue: Environment Not Changing
|
|
|
|
**Symptom:** Environment stuck on old value
|
|
|
|
**Cause:** Environment is cached
|
|
|
|
**Solution:**
|
|
```python
|
|
# Clear cache and re-detect
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
|
|
# Now check again
|
|
from app.core.environment import get_environment
|
|
print(get_environment())
|
|
```
|
|
|
|
### Issue: Tests Failing Due to Environment
|
|
|
|
**Symptom:** Tests pass locally but fail in CI/CD
|
|
|
|
**Solution:** Always clear cache in test fixtures:
|
|
```python
|
|
@pytest.fixture(autouse=True)
|
|
def clear_environment_cache():
|
|
"""Clear environment cache before each test."""
|
|
import app.core.environment as env_module
|
|
env_module._cached_environment = None
|
|
yield
|
|
env_module._cached_environment = None
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
### Quick Reference
|
|
|
|
```python
|
|
# Import
|
|
from app.core.environment import (
|
|
get_environment,
|
|
is_development,
|
|
is_production,
|
|
should_use_secure_cookies
|
|
)
|
|
|
|
# Usage
|
|
env = get_environment() # "development" | "staging" | "production"
|
|
|
|
if is_development():
|
|
# Development-only code
|
|
pass
|
|
|
|
if is_production():
|
|
# Production-only code
|
|
pass
|
|
|
|
# Cookie security (most common use case)
|
|
response.set_cookie(secure=should_use_secure_cookies())
|
|
```
|
|
|
|
### Configuration
|
|
|
|
| Environment | Set Variable | Cookie Secure | HTTPS Required |
|
|
|-------------|--------------|---------------|----------------|
|
|
| Development | Not needed (default) | False | No |
|
|
| Staging | `ENV=staging` | True | Yes |
|
|
| Production | `ENV=production` | True | Yes |
|
|
|
|
### Key Points
|
|
|
|
1. ✅ **Development works out of the box** - No configuration needed
|
|
2. ✅ **Production requires explicit setting** - Set `ENV=production`
|
|
3. ✅ **Use utility functions** - Don't check environment directly
|
|
4. ✅ **HTTPS required in production** - Configure at server level
|
|
5. ✅ **Test with different environments** - Use monkeypatch in tests
|
|
|
|
---
|
|
|
|
## Additional Resources
|
|
|
|
### Related Documentation
|
|
|
|
- [Authentication System Documentation](../api/authentication.md)
|
|
- [Deployment Guide](../deployment/production.md)
|
|
- [Exception Handling](exception-handling.md)
|
|
|
|
### External References
|
|
|
|
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
|
|
- [HTTP Cookies (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
|
|
- [12-Factor App: Config](https://12factor.net/config)
|
|
|
|
---
|
|
|
|
**Questions?** Contact the backend team or check the main documentation.
|
|
|
|
**End of Document**
|