Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 Orion 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: orion: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 <orion-repo>
|
|
cd orion-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
|
|
- Store: http://localhost:8000/store/{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/orion_staging
|
|
ports:
|
|
- "8000:8000"
|
|
depends_on:
|
|
- db
|
|
|
|
db:
|
|
image: postgres:15
|
|
environment:
|
|
- POSTGRES_DB=orion_staging
|
|
- POSTGRES_USER=user
|
|
- POSTGRES_PASSWORD=pass
|
|
```
|
|
|
|
**Kubernetes:**
|
|
```yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: orion-staging
|
|
spec:
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: web
|
|
image: orion: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=Orion Platform
|
|
After=network.target
|
|
|
|
[Service]
|
|
User=orion
|
|
WorkingDirectory=/opt/orion
|
|
Environment="ENV=production"
|
|
Environment="DATABASE_URL=postgresql://user:pass@localhost/orion_prod"
|
|
ExecStart=/opt/orion/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**
|