Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -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):

View File

@@ -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,

View File

@@ -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")

View File

@@ -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)
],

View File

@@ -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}
)

View File

@@ -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)

View 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"
)

View File

@@ -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"""

View File

@@ -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
View 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)

View 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'")

View 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
View 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)

View 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)

View 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)