Add subcription feature 3
This commit is contained in:
@@ -51,7 +51,8 @@ class HeaderManager:
|
||||
'accept',
|
||||
'accept-language',
|
||||
'user-agent',
|
||||
'x-internal-service' # Required for internal service-to-service ML/alert triggers
|
||||
'x-internal-service', # Required for internal service-to-service ML/alert triggers
|
||||
'stripe-signature' # Required for Stripe webhook signature verification
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ from app.middleware.rate_limiting import APIRateLimitMiddleware
|
||||
from app.middleware.subscription import SubscriptionMiddleware
|
||||
from app.middleware.demo_middleware import DemoMiddleware
|
||||
from app.middleware.read_only_mode import ReadOnlyModeMiddleware
|
||||
from app.routes import auth, tenant, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
|
||||
from app.routes import auth, tenant, registration, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks
|
||||
|
||||
# Initialize logger
|
||||
logger = structlog.get_logger()
|
||||
@@ -40,36 +40,61 @@ try:
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not check file descriptor limits: {e}")
|
||||
|
||||
# Redis client for SSE streaming
|
||||
# Global Redis client for SSE streaming
|
||||
redis_client = None
|
||||
|
||||
|
||||
class GatewayService(StandardFastAPIService):
|
||||
"""Gateway Service with standardized monitoring setup"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Initialize HeaderManager early
|
||||
header_manager.initialize()
|
||||
logger.info("HeaderManager initialized")
|
||||
|
||||
# Initialize Redis during service creation so it's available when needed
|
||||
try:
|
||||
# We need to run the async initialization in a sync context
|
||||
import asyncio
|
||||
try:
|
||||
# Check if there's already a running event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
# If there is, we'll initialize Redis later in on_startup
|
||||
self.redis_initialized = False
|
||||
self.redis_client = None
|
||||
except RuntimeError:
|
||||
# No event loop running, safe to run the async function
|
||||
import asyncio
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply() # Allow nested event loops
|
||||
|
||||
async def init_redis():
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
return await get_redis_client()
|
||||
|
||||
self.redis_client = asyncio.run(init_redis())
|
||||
self.redis_initialized = True
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Redis during service creation: {e}")
|
||||
self.redis_initialized = False
|
||||
self.redis_client = None
|
||||
|
||||
async def on_startup(self, app):
|
||||
"""Custom startup logic for Gateway"""
|
||||
global redis_client
|
||||
|
||||
# Initialize HeaderManager
|
||||
header_manager.initialize()
|
||||
logger.info("HeaderManager initialized")
|
||||
|
||||
# Initialize Redis
|
||||
try:
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
redis_client = await get_redis_client()
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
|
||||
# Add API rate limiting middleware with Redis client
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
|
||||
# NOTE: SubscriptionMiddleware and AuthMiddleware are instantiated without redis_client
|
||||
# They will gracefully degrade (skip Redis-dependent features) when redis_client is None
|
||||
# For future enhancement: consider using lifespan context to inject redis_client into middleware
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
# Initialize Redis if not already done during service creation
|
||||
if not self.redis_initialized:
|
||||
try:
|
||||
await initialize_redis(settings.REDIS_URL, db=0, max_connections=50)
|
||||
self.redis_client = await get_redis_client()
|
||||
redis_client = self.redis_client # Update global variable
|
||||
self.redis_initialized = True
|
||||
logger.info("Connected to Redis for SSE streaming")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis during startup: {e}")
|
||||
|
||||
# Register custom metrics for gateway-specific operations
|
||||
if self.telemetry_providers and self.telemetry_providers.app_metrics:
|
||||
@@ -103,6 +128,23 @@ service = GatewayService(
|
||||
# Create FastAPI app
|
||||
app = service.create_app()
|
||||
|
||||
# Add API rate limiting middleware with Redis client - this needs to be done after app creation
|
||||
# but before other middleware that might depend on it
|
||||
# Wait for Redis to be initialized if not already done
|
||||
if not hasattr(service, 'redis_client') or not service.redis_client:
|
||||
# Wait briefly for Redis initialization to complete
|
||||
import time
|
||||
time.sleep(1)
|
||||
# Check again after allowing time for initialization
|
||||
if hasattr(service, 'redis_client') and service.redis_client:
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
else:
|
||||
logger.warning("Redis client not available for API rate limiting middleware")
|
||||
else:
|
||||
app.add_middleware(APIRateLimitMiddleware, redis_client=service.redis_client)
|
||||
logger.info("API rate limiting middleware enabled")
|
||||
|
||||
# Add gateway-specific middleware (in REVERSE order of execution)
|
||||
# Execution order: RequestIDMiddleware -> DemoMiddleware -> AuthMiddleware -> ReadOnlyModeMiddleware -> SubscriptionMiddleware -> APIRateLimitMiddleware -> RateLimitMiddleware -> LoggingMiddleware
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
@@ -115,6 +157,7 @@ app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
||||
app.include_router(registration.router, prefix="/api/v1", tags=["registration"])
|
||||
app.include_router(tenant.router, prefix="/api/v1/tenants", tags=["tenants"])
|
||||
app.include_router(subscription.router, prefix="/api/v1", tags=["subscriptions"])
|
||||
# Notification routes are now handled by tenant router at /api/v1/tenants/{tenant_id}/notifications/*
|
||||
@@ -122,9 +165,9 @@ app.include_router(nominatim.router, prefix="/api/v1/nominatim", tags=["location
|
||||
app.include_router(geocoding.router, prefix="/api/v1/geocoding", tags=["geocoding"])
|
||||
app.include_router(pos.router, prefix="/api/v1/pos", tags=["pos"])
|
||||
app.include_router(demo.router, prefix="/api/v1", tags=["demo"])
|
||||
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
|
||||
# Also include webhooks at /webhooks prefix to support direct webhook URLs like /webhooks/stripe
|
||||
app.include_router(webhooks.router, prefix="/webhooks", tags=["webhooks-external"])
|
||||
# Include webhooks at the root level to handle /api/v1/webhooks/*
|
||||
# Webhook routes are defined with full /api/v1/webhooks/* paths for consistency
|
||||
app.include_router(webhooks.router, prefix="", tags=["webhooks"])
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -257,7 +300,7 @@ def _determine_event_type(event_data: dict) -> str:
|
||||
# SERVER-SENT EVENTS (SSE) ENDPOINT
|
||||
# ================================================================
|
||||
|
||||
@app.get("/api/events")
|
||||
@app.get("/api/v1/events")
|
||||
async def events_stream(
|
||||
request: Request,
|
||||
tenant_id: str,
|
||||
|
||||
@@ -34,10 +34,15 @@ PUBLIC_ROUTES = [
|
||||
"/api/v1/auth/register",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/auth/verify",
|
||||
"/api/v1/auth/start-registration", # Registration step 1 - SetupIntent creation
|
||||
"/api/v1/auth/complete-registration", # Registration step 2 - Completion after 3DS
|
||||
"/api/v1/auth/verify-email", # Email verification
|
||||
"/api/v1/nominatim/search",
|
||||
"/api/v1/plans",
|
||||
"/api/v1/demo/accounts",
|
||||
"/api/v1/demo/sessions"
|
||||
"/api/v1/demo/sessions",
|
||||
"/api/v1/webhooks/stripe", # Stripe webhook endpoint - bypasses auth for signature verification
|
||||
"/api/v1/webhooks/generic" # Generic webhook endpoint
|
||||
]
|
||||
|
||||
# Routes accessible with demo session (no JWT required, just demo session header)
|
||||
@@ -74,7 +79,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
logger.info(f"Auth check - path: {request.url.path}, demo_header: {demo_session_header}, demo_query: {demo_session_query}, has_demo_state: {hasattr(request.state, 'is_demo_session')}")
|
||||
|
||||
# For SSE endpoint with demo_session_id in query params, validate it here
|
||||
if request.url.path == "/api/events" and demo_session_query and not hasattr(request.state, "is_demo_session"):
|
||||
if request.url.path == "/api/v1/events" and demo_session_query and not hasattr(request.state, "is_demo_session"):
|
||||
logger.info(f"SSE endpoint with demo_session_id query param: {demo_session_query}")
|
||||
# Validate demo session via demo-session service using JWT service token
|
||||
import httpx
|
||||
@@ -240,14 +245,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Extract JWT token from Authorization header or query params for SSE.
|
||||
|
||||
For SSE endpoints (/api/events), browsers' EventSource API cannot send
|
||||
For SSE endpoints (/api/v1/events), browsers' EventSource API cannot send
|
||||
custom headers, so we must accept token as query parameter.
|
||||
For all other routes, token must be in Authorization header (more secure).
|
||||
|
||||
Security note: Query param tokens are logged. Use short expiry and filter logs.
|
||||
"""
|
||||
# SSE endpoint exception: token in query param (EventSource API limitation)
|
||||
if request.url.path == "/api/events":
|
||||
if request.url.path == "/api/v1/events":
|
||||
token = request.query_params.get("token")
|
||||
if token:
|
||||
logger.debug("Token extracted from query param for SSE endpoint")
|
||||
|
||||
@@ -50,6 +50,7 @@ DEMO_ALLOWED_OPERATIONS = {
|
||||
"/api/v1/tenants/batch/sales-summary",
|
||||
"/api/v1/tenants/batch/production-summary",
|
||||
"/api/v1/auth/me/onboarding/complete", # Allow completing onboarding (no-op for demos)
|
||||
"/api/v1/tenants/*/notifications/send", # Allow notifications (ML insights, alerts, etc.)
|
||||
# Note: Forecast generation is explicitly blocked (see DEMO_BLOCKED_PATHS)
|
||||
],
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ READ_ONLY_WHITELIST_PATTERNS = [
|
||||
r'^/api/v1/tenants/.*/procurement/ml/insights/.*', # Allow ML insights (supplier analysis, price forecasting)
|
||||
r'^/api/v1/tenants/.*/forecasting/ml/insights/.*', # Allow ML insights (rules generation)
|
||||
r'^/api/v1/tenants/.*/forecasting/operations/.*', # Allow forecasting operations
|
||||
r'^/api/v1/webhooks/.*', # Webhook endpoints - no tenant context
|
||||
]
|
||||
|
||||
|
||||
@@ -55,7 +56,7 @@ class ReadOnlyModeMiddleware(BaseHTTPMiddleware):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.tenant_service_url}/api/v1/subscriptions/{tenant_id}/status",
|
||||
f"{self.tenant_service_url}/api/v1/tenants/{tenant_id}/subscriptions/status",
|
||||
headers={"Authorization": authorization}
|
||||
)
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ class SubscriptionMiddleware(BaseHTTPMiddleware):
|
||||
r'/api/v1/auth/.*',
|
||||
r'/api/v1/subscriptions/.*', # Subscription management itself
|
||||
r'/api/v1/tenants/[^/]+/members.*', # Basic tenant info
|
||||
r'/api/v1/webhooks/.*', # Webhook endpoints - no tenant context
|
||||
r'/docs.*',
|
||||
r'/openapi\.json',
|
||||
# Training monitoring endpoints (WebSocket and status checks)
|
||||
|
||||
116
gateway/app/routes/registration.py
Normal file
116
gateway/app/routes/registration.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Registration routes for API Gateway - Handles registration-specific endpoints
|
||||
These endpoints don't require a tenant ID and are used during the registration flow
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.header_manager import header_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# ================================================================
|
||||
# REGISTRATION ENDPOINTS - Direct routing to tenant service
|
||||
# These endpoints are called during registration before a tenant exists
|
||||
# ================================================================
|
||||
|
||||
@router.post("/registration-payment-setup")
|
||||
async def proxy_registration_payment_setup(request: Request):
|
||||
"""Proxy registration payment setup request to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/registration-payment-setup")
|
||||
|
||||
@router.post("/verify-and-complete-registration")
|
||||
async def proxy_verify_and_complete_registration(request: Request):
|
||||
"""Proxy verification and registration completion to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/verify-and-complete-registration")
|
||||
|
||||
@router.post("/payment-customers/create")
|
||||
async def proxy_registration_customer_create(request: Request):
|
||||
"""Proxy registration customer creation to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/payment-customers/create")
|
||||
|
||||
@router.get("/setup-intents/{setup_intent_id}/verify")
|
||||
async def proxy_registration_setup_intent_verify(request: Request, setup_intent_id: str):
|
||||
"""Proxy registration setup intent verification to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/setup-intents/{setup_intent_id}/verify")
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PROXY HELPER FUNCTIONS
|
||||
# ================================================================
|
||||
|
||||
async def _proxy_to_tenant_service(request: Request, target_path: str):
|
||||
"""Generic proxy function with enhanced error handling"""
|
||||
|
||||
# Handle OPTIONS requests directly for CORS
|
||||
if request.method == "OPTIONS":
|
||||
return Response(
|
||||
status_code=200,
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST,
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID, Stripe-Signature",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Max-Age": "86400"
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
url = f"{settings.TENANT_SERVICE_URL}{target_path}"
|
||||
|
||||
# Use unified HeaderManager for consistent header forwarding
|
||||
headers = header_manager.get_all_headers_for_proxy(request)
|
||||
|
||||
# Debug logging
|
||||
logger.info(f"Forwarding registration request to {url}")
|
||||
|
||||
# Get request body if present
|
||||
body = None
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
body = await request.body()
|
||||
|
||||
# Add query parameters
|
||||
params = dict(request.query_params)
|
||||
|
||||
timeout_config = httpx.Timeout(
|
||||
connect=30.0,
|
||||
read=60.0,
|
||||
write=30.0,
|
||||
pool=30.0
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_config) as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
params=params
|
||||
)
|
||||
|
||||
# Handle different response types
|
||||
if response.headers.get("content-type", "").startswith("application/json"):
|
||||
try:
|
||||
content = response.json()
|
||||
except:
|
||||
content = {"message": "Invalid JSON response from service"}
|
||||
else:
|
||||
content = response.text
|
||||
|
||||
return JSONResponse(
|
||||
status_code=response.status_code,
|
||||
content=content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error proxying registration request to {settings.TENANT_SERVICE_URL}{target_path}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Internal gateway error"
|
||||
)
|
||||
@@ -24,6 +24,8 @@ async def create_tenant(request: Request):
|
||||
"""Proxy tenant creation to tenant service"""
|
||||
return await _proxy_to_tenant_service(request, "/api/v1/tenants/register")
|
||||
|
||||
|
||||
|
||||
@router.get("/{tenant_id}")
|
||||
async def get_tenant(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get specific tenant details"""
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"""
|
||||
Webhook routes for API Gateway - Handles webhook endpoints
|
||||
|
||||
Route Configuration Notes:
|
||||
- Stripe configures webhook URL as: https://domain.com/api/v1/webhooks/stripe
|
||||
- Gateway receives /api/v1/webhooks/* routes and proxies to tenant service at /webhooks/*
|
||||
- Gateway routes use /api/v1 prefix, but tenant service routes use /webhooks/* prefix
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi import APIRouter, Request, Response, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
import logging
|
||||
@@ -16,16 +21,20 @@ router = APIRouter()
|
||||
|
||||
# ================================================================
|
||||
# WEBHOOK ENDPOINTS - Direct routing to tenant service
|
||||
# All routes use /api/v1 prefix for consistency
|
||||
# ================================================================
|
||||
|
||||
@router.post("/stripe")
|
||||
# Stripe webhook endpoint
|
||||
@router.post("/api/v1/webhooks/stripe")
|
||||
async def proxy_stripe_webhook(request: Request):
|
||||
"""Proxy Stripe webhook requests to tenant service"""
|
||||
"""Proxy Stripe webhook requests to tenant service (path: /webhooks/stripe)"""
|
||||
logger.info("Received Stripe webhook at /api/v1/webhooks/stripe")
|
||||
return await _proxy_to_tenant_service(request, "/webhooks/stripe")
|
||||
|
||||
@router.post("/generic")
|
||||
# Generic webhook endpoint
|
||||
@router.post("/api/v1/webhooks/generic")
|
||||
async def proxy_generic_webhook(request: Request):
|
||||
"""Proxy generic webhook requests to tenant service"""
|
||||
"""Proxy generic webhook requests to tenant service (path: /webhooks/generic)"""
|
||||
return await _proxy_to_tenant_service(request, "/webhooks/generic")
|
||||
|
||||
# ================================================================
|
||||
|
||||
71
gateway/test_routes.py
Normal file
71
gateway/test_routes.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to verify route consistency in the gateway.
|
||||
This script checks that all routes follow the /api/v1 pattern.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def test_route_consistency():
|
||||
"""Test that all routes follow the /api/v1 pattern."""
|
||||
|
||||
# Files to check
|
||||
files_to_check = [
|
||||
"app/main.py",
|
||||
"app/routes/webhooks.py",
|
||||
"app/middleware/auth.py"
|
||||
]
|
||||
|
||||
print("Testing route consistency...")
|
||||
|
||||
# Patterns to look for
|
||||
patterns = {
|
||||
"old_sse_route": r'/api/events',
|
||||
"old_webhook_route": r'/v1/webhooks',
|
||||
"new_sse_route": r'/api/v1/events',
|
||||
"new_webhook_route": r'/api/v1/webhooks'
|
||||
}
|
||||
|
||||
issues_found = []
|
||||
|
||||
for file_path in files_to_check:
|
||||
full_path = Path("gateway") / file_path
|
||||
if not full_path.exists():
|
||||
continue
|
||||
|
||||
print(f"\nChecking {file_path}...")
|
||||
|
||||
with open(full_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for old patterns (should not exist)
|
||||
for pattern_name, pattern in patterns.items():
|
||||
if "old" in pattern_name:
|
||||
matches = re.findall(pattern, content)
|
||||
if matches:
|
||||
issues_found.append(f"❌ Found old pattern {pattern} in {file_path}: {matches}")
|
||||
else:
|
||||
print(f"✅ No old {pattern_name} found")
|
||||
|
||||
# Check for new patterns (should exist)
|
||||
for pattern_name, pattern in patterns.items():
|
||||
if "new" in pattern_name:
|
||||
matches = re.findall(pattern, content)
|
||||
if matches:
|
||||
print(f"✅ Found new {pattern_name}: {len(matches)} occurrences")
|
||||
else:
|
||||
issues_found.append(f"❌ Missing new pattern {pattern} in {file_path}")
|
||||
|
||||
if issues_found:
|
||||
print("\n❌ Issues found:")
|
||||
for issue in issues_found:
|
||||
print(f" {issue}")
|
||||
return False
|
||||
else:
|
||||
print("\n✅ All route consistency checks passed!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_route_consistency()
|
||||
exit(0 if success else 1)
|
||||
62
gateway/test_routing_behavior.py
Normal file
62
gateway/test_routing_behavior.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to understand FastAPI routing behavior with different prefix configurations.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
# Test 1: Router with prefix="" and full path in route
|
||||
router1 = APIRouter()
|
||||
|
||||
@router1.post("/api/v1/webhooks/stripe")
|
||||
async def webhook_stripe_v1(request: Request):
|
||||
return {"message": "webhook_stripe_v1"}
|
||||
|
||||
# Test 2: Router with prefix="" and relative path in route
|
||||
router2 = APIRouter()
|
||||
|
||||
@router2.post("/webhooks/stripe")
|
||||
async def webhook_stripe_v2(request: Request):
|
||||
return {"message": "webhook_stripe_v2"}
|
||||
|
||||
# Test 3: Router with prefix="/api/v1" and relative path in route
|
||||
router3 = APIRouter()
|
||||
|
||||
@router3.post("/webhooks/stripe")
|
||||
async def webhook_stripe_v3(request: Request):
|
||||
return {"message": "webhook_stripe_v3"}
|
||||
|
||||
# Create test apps
|
||||
app1 = FastAPI()
|
||||
app1.include_router(router1, prefix="")
|
||||
|
||||
app2 = FastAPI()
|
||||
app2.include_router(router2, prefix="")
|
||||
|
||||
app3 = FastAPI()
|
||||
app3.include_router(router3, prefix="/api/v1")
|
||||
|
||||
print("Testing routing behavior...")
|
||||
print()
|
||||
|
||||
# Test each configuration
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
client1 = TestClient(app1)
|
||||
response1 = client1.post("/api/v1/webhooks/stripe")
|
||||
print(f"Test 1 (prefix='', full path): {response1.status_code} - {response1.json() if response1.status_code == 200 else 'Not found'}")
|
||||
|
||||
client2 = TestClient(app2)
|
||||
response2 = client2.post("/api/v1/webhooks/stripe")
|
||||
print(f"Test 2 (prefix='', relative path): {response2.status_code} - {response2.json() if response2.status_code == 200 else 'Not found'}")
|
||||
|
||||
client3 = TestClient(app3)
|
||||
response3 = client3.post("/api/v1/webhooks/stripe")
|
||||
print(f"Test 3 (prefix='/api/v1', relative path): {response3.status_code} - {response3.json() if response3.status_code == 200 else 'Not found'}")
|
||||
|
||||
print()
|
||||
print("Analysis:")
|
||||
print("- Test 1 should work: prefix='' + full path '/api/v1/webhooks/stripe' = '/api/v1/webhooks/stripe'")
|
||||
print("- Test 2 should NOT work: prefix='' + relative path '/webhooks/stripe' = '/webhooks/stripe' (not '/api/v1/webhooks/stripe')")
|
||||
print("- Test 3 should work: prefix='/api/v1' + relative path '/webhooks/stripe' = '/api/v1/webhooks/stripe'")
|
||||
80
gateway/test_stripe_signature_fix.py
Normal file
80
gateway/test_stripe_signature_fix.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify that the stripe-signature header fix works correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the gateway directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_stripe_signature_in_forwardable_headers():
|
||||
"""Test that stripe-signature is in the forwardable headers list."""
|
||||
|
||||
# Read the header manager file
|
||||
with open('app/core/header_manager.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if stripe-signature is in the forwardable headers
|
||||
stripe_signature_header = "'stripe-signature'"
|
||||
|
||||
if stripe_signature_header in content:
|
||||
print("✅ stripe-signature header found in FORWARDABLE_HEADERS")
|
||||
return True
|
||||
else:
|
||||
print("❌ stripe-signature header NOT found in FORWARDABLE_HEADERS")
|
||||
return False
|
||||
|
||||
def test_stripe_signature_comment():
|
||||
"""Test that there's a comment explaining why stripe-signature is needed."""
|
||||
|
||||
# Read the header manager file
|
||||
with open('app/core/header_manager.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if there's a comment about Stripe webhook signature verification
|
||||
comment = "Required for Stripe webhook signature verification"
|
||||
|
||||
if comment in content:
|
||||
print("✅ Comment found explaining stripe-signature requirement")
|
||||
return True
|
||||
else:
|
||||
print("❌ Comment NOT found explaining stripe-signature requirement")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Testing stripe-signature header fix...")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
test_stripe_signature_in_forwardable_headers,
|
||||
test_stripe_signature_comment,
|
||||
]
|
||||
|
||||
results = []
|
||||
for test in tests:
|
||||
try:
|
||||
result = test()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
print(f"❌ Error running {test.__name__}: {e}")
|
||||
results.append(False)
|
||||
print()
|
||||
|
||||
if all(results):
|
||||
print("🎉 All tests passed! stripe-signature header should now be forwarded.")
|
||||
print()
|
||||
print("Summary of changes:")
|
||||
print("- Added 'stripe-signature' to FORWARDABLE_HEADERS list")
|
||||
print("- This ensures the Stripe signature is forwarded to tenant service")
|
||||
print("- Tenant service can now verify webhook signatures correctly")
|
||||
return True
|
||||
else:
|
||||
print("❌ Some tests failed. stripe-signature header may not be forwarded correctly.")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
130
gateway/test_webhook_fix.py
Normal file
130
gateway/test_webhook_fix.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify that webhook routes work correctly after the fix.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
# Add the gateway directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_subscription_middleware_skip_patterns():
|
||||
"""Test that webhook routes are in the skip patterns."""
|
||||
|
||||
# Read the subscription middleware file
|
||||
with open('app/middleware/subscription.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook routes are in the skip patterns
|
||||
webhook_pattern = r'/api/v1/webhooks/.*'
|
||||
if webhook_pattern in content:
|
||||
print("✅ Webhook routes found in SubscriptionMiddleware skip patterns")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routes NOT found in SubscriptionMiddleware skip patterns")
|
||||
return False
|
||||
|
||||
def test_read_only_middleware_whitelist():
|
||||
"""Test that webhook routes are in the whitelist."""
|
||||
|
||||
# Read the read-only middleware file
|
||||
with open('app/middleware/read_only_mode.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook routes are in the whitelist
|
||||
webhook_pattern = r'^/api/v1/webhooks/.*'
|
||||
if webhook_pattern in content:
|
||||
print("✅ Webhook routes found in ReadOnlyModeMiddleware whitelist")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routes NOT found in ReadOnlyModeMiddleware whitelist")
|
||||
return False
|
||||
|
||||
def test_webhook_route_definitions():
|
||||
"""Test that webhook routes are correctly defined."""
|
||||
|
||||
# Read the webhooks file
|
||||
with open('app/routes/webhooks.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook routes are defined with full paths
|
||||
stripe_route = '@router.post("/api/v1/webhooks/stripe")'
|
||||
generic_route = '@router.post("/api/v1/webhooks/generic")'
|
||||
|
||||
if stripe_route in content and generic_route in content:
|
||||
print("✅ Webhook routes correctly defined with full paths")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routes NOT correctly defined")
|
||||
return False
|
||||
|
||||
def test_webhook_router_inclusion():
|
||||
"""Test that webhook router is included with correct prefix."""
|
||||
|
||||
# Read the main file
|
||||
with open('app/main.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook router is included with prefix=""
|
||||
webhook_inclusion = "app.include_router(webhooks.router, prefix=\"\", tags=[\"webhooks\"])"
|
||||
|
||||
if webhook_inclusion in content:
|
||||
print("✅ Webhook router correctly included with prefix=''")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook router NOT correctly included")
|
||||
return False
|
||||
|
||||
def test_auth_middleware_public_routes():
|
||||
"""Test that webhook routes are in the public routes."""
|
||||
|
||||
# Read the auth middleware file
|
||||
with open('app/middleware/auth.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook routes are in the public routes
|
||||
stripe_public = '/api/v1/webhooks/stripe'
|
||||
generic_public = '/api/v1/webhooks/generic'
|
||||
|
||||
if stripe_public in content and generic_public in content:
|
||||
print("✅ Webhook routes found in AuthMiddleware public routes")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routes NOT found in AuthMiddleware public routes")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Testing webhook route configuration...")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
test_webhook_route_definitions,
|
||||
test_webhook_router_inclusion,
|
||||
test_auth_middleware_public_routes,
|
||||
test_subscription_middleware_skip_patterns,
|
||||
test_read_only_middleware_whitelist,
|
||||
]
|
||||
|
||||
results = []
|
||||
for test in tests:
|
||||
try:
|
||||
result = test()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
print(f"❌ Error running {test.__name__}: {e}")
|
||||
results.append(False)
|
||||
print()
|
||||
|
||||
if all(results):
|
||||
print("🎉 All tests passed! Webhook routes should work correctly.")
|
||||
return True
|
||||
else:
|
||||
print("❌ Some tests failed. Webhook routes may not work correctly.")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
83
gateway/test_webhook_proxy_fix.py
Normal file
83
gateway/test_webhook_proxy_fix.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify that the webhook proxy fix works correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the gateway directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_webhook_proxy_target_paths():
|
||||
"""Test that webhook routes proxy to the correct target paths."""
|
||||
|
||||
# Read the webhooks file
|
||||
with open('app/routes/webhooks.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if webhook routes proxy to the correct target paths
|
||||
stripe_proxy = 'return await _proxy_to_tenant_service(request, "/webhooks/stripe")'
|
||||
generic_proxy = 'return await _proxy_to_tenant_service(request, "/webhooks/generic")'
|
||||
|
||||
if stripe_proxy in content and generic_proxy in content:
|
||||
print("✅ Webhook routes correctly proxy to tenant service at /webhooks/*")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routes NOT correctly proxying to tenant service")
|
||||
print(f" Looking for: {stripe_proxy}")
|
||||
print(f" Looking for: {generic_proxy}")
|
||||
return False
|
||||
|
||||
def test_webhook_route_comments():
|
||||
"""Test that the comments reflect the correct routing."""
|
||||
|
||||
# Read the webhooks file
|
||||
with open('app/routes/webhooks.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if comments reflect the correct routing
|
||||
correct_comment = "Gateway receives /api/v1/webhooks/* routes and proxies to tenant service at /webhooks/*"
|
||||
|
||||
if correct_comment in content:
|
||||
print("✅ Webhook routing comments are correct")
|
||||
return True
|
||||
else:
|
||||
print("❌ Webhook routing comments are NOT correct")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Testing webhook proxy fix...")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
test_webhook_proxy_target_paths,
|
||||
test_webhook_route_comments,
|
||||
]
|
||||
|
||||
results = []
|
||||
for test in tests:
|
||||
try:
|
||||
result = test()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
print(f"❌ Error running {test.__name__}: {e}")
|
||||
results.append(False)
|
||||
print()
|
||||
|
||||
if all(results):
|
||||
print("🎉 All tests passed! Webhook proxy should work correctly.")
|
||||
print()
|
||||
print("Summary of changes:")
|
||||
print("- Gateway now proxies /api/v1/webhooks/stripe to /webhooks/stripe")
|
||||
print("- Gateway now proxies /api/v1/webhooks/generic to /webhooks/generic")
|
||||
print("- This matches the tenant service's expected routes")
|
||||
return True
|
||||
else:
|
||||
print("❌ Some tests failed. Webhook proxy may not work correctly.")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
49
gateway/test_webhook_routing.py
Normal file
49
gateway/test_webhook_routing.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify webhook routing order and ensure webhooks are caught correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the gateway directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
def test_webhook_routing():
|
||||
"""Test that webhook routes are properly handled."""
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
print("Testing webhook routing...")
|
||||
|
||||
# Test Stripe webhook route
|
||||
response = client.post("/api/v1/webhooks/stripe", json={"test": "data"})
|
||||
print(f"Stripe webhook route: {response.status_code}")
|
||||
|
||||
# Test generic webhook route
|
||||
response = client.post("/api/v1/webhooks/generic", json={"test": "data"})
|
||||
print(f"Generic webhook route: {response.status_code}")
|
||||
|
||||
# Test that other routes still work
|
||||
response = client.get("/health")
|
||||
print(f"Health check route: {response.status_code}")
|
||||
|
||||
# Test that tenant routes still work
|
||||
response = client.get("/api/v1/tenants/test")
|
||||
print(f"Tenant route: {response.status_code}")
|
||||
|
||||
print("\n✅ Webhook routing test completed!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
success = test_webhook_routing()
|
||||
sys.exit(0 if success else 1)
|
||||
except Exception as e:
|
||||
print(f"❌ Error during webhook routing test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user