Files
service-finder/backend/app/tests/e2e/test_user_registration_flow.py
2026-03-22 11:02:05 +00:00

256 lines
9.5 KiB
Python

"""
End-to-end test for user registration flow with Mailpit email interception.
This test validates the complete user journey:
1. Register with a unique email (Lite registration)
2. Intercept activation email via Mailpit API
3. Extract verification token and call verify-email endpoint
4. Login with credentials
5. Complete KYC with dummy data
6. Verify gamification endpoint returns 200 OK
"""
import asyncio
import httpx
import pytest
import uuid
import re
import logging
from typing import Dict, Optional
from datetime import date
logger = logging.getLogger(__name__)
# Configuration
BASE_URL = "http://sf_api:8000"
MAILPIT_URL = "http://sf_mailpit:8025"
TEST_EMAIL_DOMAIN = "example.com"
class MailpitClient:
"""Client for interacting with Mailpit API."""
def __init__(self, base_url: str = MAILPIT_URL):
self.base_url = base_url
self.client = httpx.AsyncClient(timeout=30.0)
async def get_latest_message(self) -> Optional[Dict]:
"""Fetch the latest email message from Mailpit."""
try:
response = await self.client.get(f"{self.base_url}/api/v1/messages?limit=1")
response.raise_for_status()
data = response.json()
if data.get("messages"):
return data["messages"][0]
return None
except Exception as e:
logger.error(f"Failed to fetch latest message: {e}")
return None
async def get_message_content(self, message_id: str) -> Optional[str]:
"""Get the full content (HTML and text) of a specific message."""
try:
response = await self.client.get(f"{self.base_url}/api/v1/message/{message_id}")
response.raise_for_status()
data = response.json()
# Prefer text over HTML
return data.get("Text") or data.get("HTML") or ""
except Exception as e:
logger.error(f"Failed to fetch message content: {e}")
return None
async def cleanup(self):
"""Close the HTTP client."""
await self.client.aclose()
class APIClient:
"""Client for interacting with the Service Finder API."""
def __init__(self, base_url: str = BASE_URL):
self.base_url = base_url
self.client = httpx.AsyncClient(timeout=30.0)
self.token = None
async def register(self, email: str, password: str = "TestPassword123!") -> httpx.Response:
"""Register a new user."""
payload = {
"email": email,
"password": password,
"first_name": "Test",
"last_name": "User",
"region_code": "HU",
"lang": "hu"
}
response = await self.client.post(f"{self.base_url}/api/v1/auth/register", json=payload)
return response
async def login(self, email: str, password: str = "TestPassword123!") -> Optional[str]:
"""Login and return JWT token."""
payload = {
"username": email,
"password": password
}
response = await self.client.post(f"{self.base_url}/api/v1/auth/login", data=payload)
if response.status_code == 200:
data = response.json()
self.token = data.get("access_token")
return self.token
return None
async def verify_email(self, token: str) -> httpx.Response:
"""Call verify-email endpoint with token."""
payload = {"token": token}
response = await self.client.post(f"{self.base_url}/api/v1/auth/verify-email", json=payload)
return response
async def complete_kyc(self, kyc_data: Dict) -> httpx.Response:
"""Complete KYC for current user."""
headers = {}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
response = await self.client.post(f"{self.base_url}/api/v1/auth/complete-kyc", json=kyc_data, headers=headers)
return response
async def get_gamification(self) -> httpx.Response:
"""Get gamification data for current user."""
headers = {}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
response = await self.client.get(f"{self.base_url}/api/v1/gamification/me", headers=headers)
return response
async def cleanup(self):
"""Close the HTTP client."""
await self.client.aclose()
def extract_verification_token(text: str) -> Optional[str]:
"""
Extract verification token from email text using regex.
Looks for UUID patterns in URLs or plain text.
"""
# Pattern for UUID (version 4)
uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
match = re.search(uuid_pattern, text, re.IGNORECASE)
if match:
return match.group(0)
# Fallback: look for token parameter in URL
token_pattern = r'(?:token|code)=([0-9a-f\-]+)'
match = re.search(token_pattern, text, re.IGNORECASE)
if match:
return match.group(1)
return None
def create_dummy_kyc_data() -> Dict:
"""Create dummy KYC data for testing."""
return {
"phone_number": "+36123456789",
"birth_place": "Budapest",
"birth_date": "1990-01-01",
"mothers_last_name": "Kovács",
"mothers_first_name": "Éva",
"address_zip": "1011",
"address_city": "Budapest",
"address_street_name": "Kossuth",
"address_street_type": "utca",
"address_house_number": "1",
"address_stairwell": "A",
"address_floor": "2",
"address_door": "3",
"address_hrsz": None,
"identity_docs": {
"ID_CARD": {
"number": "123456AB",
"expiry_date": "2030-12-31"
}
},
"ice_contact": {
"name": "Test Contact",
"phone": "+36198765432",
"relationship": "parent"
},
"preferred_language": "hu",
"preferred_currency": "HUF"
}
@pytest.mark.asyncio
async def test_user_registration_flow():
"""Main E2E test for user registration flow."""
# Generate unique test email
test_email = f"test_{uuid.uuid4().hex[:8]}@{TEST_EMAIL_DOMAIN}"
logger.info(f"Using test email: {test_email}")
# Initialize clients
api_client = APIClient()
mailpit = MailpitClient()
try:
# 1. Register new user (Lite registration)
logger.info("Step 1: Registering user")
reg_response = await api_client.register(test_email)
assert reg_response.status_code in (200, 201, 202), f"Registration failed: {reg_response.status_code} - {reg_response.text}"
logger.info(f"Registration response: {reg_response.status_code}")
# 2. Wait for email (Mailpit may need a moment)
await asyncio.sleep(2)
# 3. Fetch latest email from Mailpit
logger.info("Step 2: Fetching email from Mailpit")
message = await mailpit.get_latest_message()
assert message is not None, "No email found in Mailpit"
logger.info(f"Found email with ID: {message.get('ID')}, Subject: {message.get('Subject')}")
# 4. Get email content and extract verification token
content = await mailpit.get_message_content(message["ID"])
assert content, "Email content is empty"
token = extract_verification_token(content)
assert token is not None, f"Could not extract verification token from email content: {content[:500]}"
logger.info(f"Extracted verification token: {token}")
# 5. Verify email using the token
logger.info("Step 3: Verifying email")
verify_response = await api_client.verify_email(token)
assert verify_response.status_code in (200, 201, 202), f"Email verification failed: {verify_response.status_code} - {verify_response.text}"
logger.info(f"Email verification response: {verify_response.status_code}")
# 6. Login to get JWT token
logger.info("Step 4: Logging in")
token = await api_client.login(test_email)
assert token is not None, "Login failed - no token received"
logger.info("Login successful, token obtained")
# 7. Complete KYC with dummy data
logger.info("Step 5: Completing KYC")
kyc_data = create_dummy_kyc_data()
kyc_response = await api_client.complete_kyc(kyc_data)
assert kyc_response.status_code in (200, 201, 202), f"KYC completion failed: {kyc_response.status_code} - {kyc_response.text}"
logger.info(f"KYC completion response: {kyc_response.status_code}")
# 8. Verify gamification endpoint
logger.info("Step 6: Checking gamification endpoint")
gamification_response = await api_client.get_gamification()
assert gamification_response.status_code == 200, f"Gamification endpoint failed: {gamification_response.status_code} - {gamification_response.text}"
logger.info(f"Gamification response: {gamification_response.status_code}")
# Optional: Validate response structure
gamification_data = gamification_response.json()
assert "points" in gamification_data or "level" in gamification_data or "achievements" in gamification_data, \
"Gamification response missing expected fields"
logger.info("✅ All steps passed! User registration flow works end-to-end.")
finally:
# Cleanup
await api_client.cleanup()
await mailpit.cleanup()
if __name__ == "__main__":
# For manual testing
import sys
logging.basicConfig(level=logging.INFO)
asyncio.run(test_user_registration_flow())