2026-01-13 22:22:38 +01:00
|
|
|
"""
|
|
|
|
|
Integration test for the complete subscription creation flow
|
|
|
|
|
Tests user registration, subscription creation, tenant creation, and linking
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import asyncio
|
|
|
|
|
import httpx
|
|
|
|
|
import stripe
|
|
|
|
|
import os
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SubscriptionCreationFlowTester:
|
|
|
|
|
"""Test the complete subscription creation flow"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.base_url = "https://bakery-ia.local"
|
|
|
|
|
self.timeout = 30.0
|
|
|
|
|
self.test_user_email = f"test_{datetime.now().strftime('%Y%m%d%H%M%S')}@example.com"
|
|
|
|
|
self.test_user_password = "SecurePassword123!"
|
|
|
|
|
self.test_user_full_name = "Test User"
|
|
|
|
|
self.test_plan_id = "starter" # Valid plans: starter, professional, enterprise
|
|
|
|
|
self.test_payment_method_id = None # Will be created dynamically
|
|
|
|
|
|
|
|
|
|
# Initialize Stripe with API key from environment
|
|
|
|
|
stripe_key = os.environ.get('STRIPE_SECRET_KEY')
|
|
|
|
|
if stripe_key:
|
|
|
|
|
stripe.api_key = stripe_key
|
|
|
|
|
print(f"✅ Stripe initialized with test mode API key")
|
|
|
|
|
else:
|
|
|
|
|
print(f"⚠️ Warning: STRIPE_SECRET_KEY not found in environment")
|
|
|
|
|
|
|
|
|
|
# Store created resources for cleanup
|
|
|
|
|
self.created_user_id = None
|
|
|
|
|
self.created_subscription_id = None
|
|
|
|
|
self.created_tenant_id = None
|
|
|
|
|
self.created_payment_method_id = None
|
|
|
|
|
|
|
|
|
|
def _create_test_payment_method(self) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Create a real Stripe test payment method using Stripe's pre-made test tokens
|
|
|
|
|
This simulates what happens in production when a user enters their card details
|
|
|
|
|
|
|
|
|
|
In production: Frontend uses Stripe.js to tokenize card → creates PaymentMethod
|
|
|
|
|
In testing: We use Stripe's pre-made test tokens (tok_visa, tok_mastercard, etc.)
|
|
|
|
|
|
|
|
|
|
See: https://stripe.com/docs/testing#cards
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
print(f"💳 Creating Stripe test payment method...")
|
|
|
|
|
|
|
|
|
|
# Use Stripe's pre-made test token tok_visa
|
|
|
|
|
# This is the recommended approach for testing and mimics production flow
|
|
|
|
|
# In production, Stripe.js creates a similar token from card details
|
|
|
|
|
payment_method = stripe.PaymentMethod.create(
|
|
|
|
|
type="card",
|
|
|
|
|
card={"token": "tok_visa"} # Stripe's pre-made test token
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.created_payment_method_id = payment_method.id
|
|
|
|
|
print(f"✅ Created Stripe test payment method: {payment_method.id}")
|
|
|
|
|
print(f" This simulates a real card in production")
|
|
|
|
|
return payment_method.id
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"❌ Failed to create payment method: {str(e)}")
|
|
|
|
|
print(f" Tip: Ensure raw card API is enabled in Stripe dashboard:")
|
|
|
|
|
print(f" https://dashboard.stripe.com/settings/integration")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
async def test_complete_flow(self):
|
|
|
|
|
"""Test the complete subscription creation flow"""
|
|
|
|
|
print(f"🧪 Starting subscription creation flow test for {self.test_user_email}")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Step 0: Create a real Stripe test payment method
|
|
|
|
|
# This is EXACTLY what happens in production when user enters card details
|
|
|
|
|
self.test_payment_method_id = self._create_test_payment_method()
|
|
|
|
|
print(f"✅ Step 0: Test payment method created")
|
|
|
|
|
|
|
|
|
|
# Step 1: Register user with subscription
|
|
|
|
|
user_data = await self._register_user_with_subscription()
|
|
|
|
|
print(f"✅ Step 1: User registered successfully - user_id: {user_data['user']['id']}")
|
|
|
|
|
|
|
|
|
|
# Step 2: Verify user was created in database
|
|
|
|
|
await self._verify_user_in_database(user_data['user']['id'])
|
|
|
|
|
print(f"✅ Step 2: User verified in database")
|
|
|
|
|
|
|
|
|
|
# Step 3: Verify subscription was created (tenant-independent)
|
|
|
|
|
subscription_data = await self._verify_subscription_created(user_data['user']['id'])
|
|
|
|
|
print(f"✅ Step 3: Tenant-independent subscription verified - subscription_id: {subscription_data['subscription_id']}")
|
|
|
|
|
|
|
|
|
|
# Step 4: Create tenant and link subscription
|
|
|
|
|
tenant_data = await self._create_tenant_and_link_subscription(user_data['user']['id'], subscription_data['subscription_id'])
|
|
|
|
|
print(f"✅ Step 4: Tenant created and subscription linked - tenant_id: {tenant_data['tenant_id']}")
|
|
|
|
|
|
|
|
|
|
# Step 5: Verify subscription is linked to tenant
|
|
|
|
|
await self._verify_subscription_linked_to_tenant(subscription_data['subscription_id'], tenant_data['tenant_id'])
|
|
|
|
|
print(f"✅ Step 5: Subscription-tenant link verified")
|
|
|
|
|
|
|
|
|
|
# Step 6: Verify tenant can access subscription
|
|
|
|
|
await self._verify_tenant_subscription_access(tenant_data['tenant_id'])
|
|
|
|
|
print(f"✅ Step 6: Tenant subscription access verified")
|
|
|
|
|
|
|
|
|
|
print(f"🎉 All tests passed! Complete flow working correctly")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"❌ Test failed: {str(e)}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
# Cleanup (optional - comment out if you want to inspect the data)
|
|
|
|
|
# await self._cleanup_resources()
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def _register_user_with_subscription(self) -> Dict[str, Any]:
|
|
|
|
|
"""Register a new user with subscription"""
|
|
|
|
|
url = f"{self.base_url}/api/v1/auth/register-with-subscription"
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"email": self.test_user_email,
|
|
|
|
|
"password": self.test_user_password,
|
|
|
|
|
"full_name": self.test_user_full_name,
|
|
|
|
|
"subscription_plan": self.test_plan_id,
|
|
|
|
|
"payment_method_id": self.test_payment_method_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
response = await client.post(url, json=payload)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
error_msg = f"User registration failed: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
self.created_user_id = result['user']['id']
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
async def _verify_user_in_database(self, user_id: str):
|
|
|
|
|
"""Verify user was created in the database"""
|
|
|
|
|
# This would be a direct database query in a real test
|
|
|
|
|
# For now, we'll just check that the user ID is valid
|
|
|
|
|
if not user_id or len(user_id) != 36: # UUID should be 36 characters
|
|
|
|
|
raise Exception(f"Invalid user ID: {user_id}")
|
|
|
|
|
|
|
|
|
|
print(f"📋 User ID validated: {user_id}")
|
|
|
|
|
|
|
|
|
|
async def _verify_subscription_created(self, user_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""Verify that a tenant-independent subscription was created"""
|
|
|
|
|
# Check the onboarding progress to see if subscription data was stored
|
|
|
|
|
url = f"{self.base_url}/api/v1/auth/me/onboarding/progress"
|
|
|
|
|
|
|
|
|
|
# Get access token for the user
|
|
|
|
|
access_token = await self._get_user_access_token()
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
error_msg = f"Failed to get onboarding progress: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
progress_data = response.json()
|
|
|
|
|
|
|
|
|
|
# Check if subscription data is in the progress
|
|
|
|
|
subscription_data = None
|
|
|
|
|
for step in progress_data.get('steps', []):
|
|
|
|
|
if step.get('step_name') == 'subscription':
|
|
|
|
|
subscription_data = step.get('step_data', {})
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not subscription_data:
|
|
|
|
|
raise Exception("No subscription data found in onboarding progress")
|
|
|
|
|
|
|
|
|
|
# Store subscription ID for later steps
|
|
|
|
|
subscription_id = subscription_data.get('subscription_id')
|
|
|
|
|
if not subscription_id:
|
|
|
|
|
raise Exception("No subscription ID found in onboarding progress")
|
|
|
|
|
|
|
|
|
|
self.created_subscription_id = subscription_id
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'subscription_id': subscription_id,
|
|
|
|
|
'plan_id': subscription_data.get('plan_id'),
|
|
|
|
|
'payment_method_id': subscription_data.get('payment_method_id'),
|
|
|
|
|
'billing_cycle': subscription_data.get('billing_cycle')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async def _get_user_access_token(self) -> str:
|
|
|
|
|
"""Get access token for the test user"""
|
|
|
|
|
url = f"{self.base_url}/api/v1/auth/login"
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"email": self.test_user_email,
|
|
|
|
|
"password": self.test_user_password
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
response = await client.post(url, json=payload)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
error_msg = f"User login failed: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
return result['access_token']
|
|
|
|
|
|
|
|
|
|
async def _create_tenant_and_link_subscription(self, user_id: str, subscription_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""Create a tenant and link the subscription to it"""
|
|
|
|
|
# This would typically be done during the onboarding flow
|
|
|
|
|
# For testing purposes, we'll simulate this by calling the tenant service directly
|
|
|
|
|
|
|
|
|
|
url = f"{self.base_url}/api/v1/tenants"
|
|
|
|
|
|
|
|
|
|
# Get access token for the user
|
|
|
|
|
access_token = await self._get_user_access_token()
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"name": f"Test Bakery {datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
|
|
|
"description": "Test bakery for integration testing",
|
|
|
|
|
"subscription_id": subscription_id,
|
|
|
|
|
"user_id": user_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
headers = {
|
|
|
|
|
"Authorization": f"Bearer {access_token}",
|
|
|
|
|
"Content-Type": "application/json"
|
|
|
|
|
}
|
|
|
|
|
response = await client.post(url, json=payload, headers=headers)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 201:
|
|
|
|
|
error_msg = f"Tenant creation failed: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
self.created_tenant_id = result['id']
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'tenant_id': result['id'],
|
|
|
|
|
'name': result['name'],
|
|
|
|
|
'status': result['status']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async def _verify_subscription_linked_to_tenant(self, subscription_id: str, tenant_id: str):
|
|
|
|
|
"""Verify that the subscription is properly linked to the tenant"""
|
2026-01-16 15:19:34 +01:00
|
|
|
url = f"{self.base_url}/api/v1/tenants/{tenant_id}/subscription/status"
|
2026-01-13 22:22:38 +01:00
|
|
|
|
|
|
|
|
# Get access token for the user
|
|
|
|
|
access_token = await self._get_user_access_token()
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
error_msg = f"Failed to get subscription status: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
subscription_status = response.json()
|
|
|
|
|
|
|
|
|
|
# Verify that the subscription is active and linked to the tenant
|
|
|
|
|
if subscription_status['status'] not in ['active', 'trialing']:
|
|
|
|
|
raise Exception(f"Subscription status is {subscription_status['status']}, expected 'active' or 'trialing'")
|
|
|
|
|
|
|
|
|
|
if subscription_status['tenant_id'] != tenant_id:
|
|
|
|
|
raise Exception(f"Subscription linked to wrong tenant: {subscription_status['tenant_id']} != {tenant_id}")
|
|
|
|
|
|
|
|
|
|
print(f"📋 Subscription status verified: {subscription_status['status']}")
|
|
|
|
|
print(f"📋 Subscription linked to tenant: {subscription_status['tenant_id']}")
|
|
|
|
|
|
|
|
|
|
async def _verify_tenant_subscription_access(self, tenant_id: str):
|
|
|
|
|
"""Verify that the tenant can access its subscription"""
|
2026-01-16 15:19:34 +01:00
|
|
|
url = f"{self.base_url}/api/v1/tenants/{tenant_id}/subscription/details"
|
2026-01-13 22:22:38 +01:00
|
|
|
|
|
|
|
|
# Get access token for the user
|
|
|
|
|
access_token = await self._get_user_access_token()
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout, verify=False) as client:
|
|
|
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
error_msg = f"Failed to get active subscription: {response.status_code} - {response.text}"
|
|
|
|
|
print(f"🚨 {error_msg}")
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
subscription_data = response.json()
|
|
|
|
|
|
|
|
|
|
# Verify that the subscription data is complete
|
|
|
|
|
required_fields = ['id', 'status', 'plan', 'current_period_start', 'current_period_end']
|
|
|
|
|
for field in required_fields:
|
|
|
|
|
if field not in subscription_data:
|
|
|
|
|
raise Exception(f"Missing required field in subscription data: {field}")
|
|
|
|
|
|
|
|
|
|
print(f"📋 Active subscription verified for tenant {tenant_id}")
|
|
|
|
|
print(f"📋 Subscription plan: {subscription_data['plan']}")
|
|
|
|
|
print(f"📋 Subscription status: {subscription_data['status']}")
|
|
|
|
|
|
|
|
|
|
async def _cleanup_resources(self):
|
|
|
|
|
"""Clean up test resources"""
|
|
|
|
|
print("🧹 Cleaning up test resources...")
|
|
|
|
|
|
|
|
|
|
# In a real test, you would delete the user, tenant, and subscription
|
|
|
|
|
# For now, we'll just print what would be cleaned up
|
|
|
|
|
print(f"Would delete user: {self.created_user_id}")
|
|
|
|
|
print(f"Would delete subscription: {self.created_subscription_id}")
|
|
|
|
|
print(f"Would delete tenant: {self.created_tenant_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_subscription_creation_flow():
|
|
|
|
|
"""Test the complete subscription creation flow"""
|
|
|
|
|
tester = SubscriptionCreationFlowTester()
|
|
|
|
|
result = await tester.test_complete_flow()
|
|
|
|
|
assert result is True, "Subscription creation flow test failed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
# Run the test
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
print("🚀 Starting subscription creation flow integration test...")
|
|
|
|
|
|
|
|
|
|
# Create and run the test
|
|
|
|
|
tester = SubscriptionCreationFlowTester()
|
|
|
|
|
|
|
|
|
|
# Run the test
|
|
|
|
|
success = asyncio.run(tester.test_complete_flow())
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
print("\n🎉 Integration test completed successfully!")
|
|
|
|
|
print("\nTest Summary:")
|
|
|
|
|
print(f"✅ User registration with subscription")
|
|
|
|
|
print(f"✅ User verification in database")
|
|
|
|
|
print(f"✅ Tenant-independent subscription creation")
|
|
|
|
|
print(f"✅ Tenant creation and subscription linking")
|
|
|
|
|
print(f"✅ Subscription-tenant link verification")
|
|
|
|
|
print(f"✅ Tenant subscription access verification")
|
|
|
|
|
print(f"\nAll components working together correctly! 🚀")
|
|
|
|
|
else:
|
|
|
|
|
print("\n❌ Integration test failed!")
|
|
|
|
|
exit(1)
|