Add subcription feature
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
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"""
|
||||
url = f"{self.base_url}/api/v1/subscriptions/{tenant_id}/status"
|
||||
|
||||
# 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"""
|
||||
url = f"{self.base_url}/api/v1/subscriptions/{tenant_id}/active"
|
||||
|
||||
# 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)
|
||||
Reference in New Issue
Block a user