#!/usr/bin/env python3 """ Sandbox Seeder Script - Creates a persistent sandbox user in the live/dev database for manual testing via Swagger. Steps: 1. Register via POST /api/v1/auth/register 2. Extract verification token from Mailpit API 3. Verify email via POST /api/v1/auth/verify-email 4. Login via POST /api/v1/auth/login to get JWT 5. Complete KYC via POST /api/v1/auth/complete-kyc 6. Create organization via POST /api/v1/organizations/onboard 7. Add a test vehicle/asset via appropriate endpoint 8. Add a fuel expense (15,000 HUF) via POST /api/v1/expenses/add Prints credentials and IDs for immediate use. """ import asyncio import httpx import json import sys import time from datetime import date, datetime, timedelta import uuid # Configuration API_BASE = "http://localhost:8000" # FastAPI server (runs inside sf_api container) MAILPIT_API = "http://sf_mailpit:8025/api/v1/messages" MAILPIT_DELETE_ALL = "http://sf_mailpit:8025/api/v1/messages" # Generate unique email each run to avoid duplicate key errors unique_id = int(time.time()) SANDBOX_EMAIL = f"sandbox_{unique_id}@test.com" SANDBOX_PASSWORD = "Sandbox123!" SANDBOX_FIRST_NAME = "Sandbox" SANDBOX_LAST_NAME = "User" # Dummy KYC data DUMMY_KYC = { "phone_number": "+36123456789", "birth_place": "Budapest", "birth_date": "1990-01-01", "mothers_last_name": "Kovรกcs", "mothers_first_name": "ร‰va", "address_zip": "1051", "address_city": "Budapest", "address_street_name": "Vรกci", "address_street_type": "utca", "address_house_number": "1", "address_stairwell": None, "address_floor": None, "address_door": None, "address_hrsz": None, "identity_docs": { "ID_CARD": { "number": "123456AB", "expiry_date": "2030-12-31" } }, "ice_contact": { "name": "John Doe", "phone": "+36198765432", "relationship": "friend" }, "preferred_language": "hu", "preferred_currency": "HUF" } # Dummy organization data DUMMY_ORG = { "full_name": "Sandbox Test Kft.", "name": "Sandbox Kft.", "display_name": "Sandbox Test", "tax_number": f"{unique_id}"[:8] + "-1-42", "reg_number": f"01-09-{unique_id}"[:6], "country_code": "HU", "language": "hu", "default_currency": "HUF", "address_zip": "1051", "address_city": "Budapest", "address_street_name": "Vรกci", "address_street_type": "utca", "address_house_number": "2", "address_stairwell": None, "address_floor": None, "address_door": None, "address_hrsz": None, "contacts": [ { "full_name": "Sandbox User", "email": SANDBOX_EMAIL, "phone": "+36123456789", "contact_type": "primary" } ] } # Dummy vehicle data DUMMY_VEHICLE = { "catalog_id": 1, # Assuming there's at least one catalog entry "license_plate": f"SBX-{uuid.uuid4().hex[:4]}".upper(), "vin": f"VIN{uuid.uuid4().hex[:10]}".upper(), "nickname": "Sandbox Car", "purchase_date": "2025-01-01", "initial_mileage": 5000, "fuel_type": "petrol", "transmission": "manual" } # Dummy expense data DUMMY_EXPENSE = { "asset_id": None, # Will be filled after vehicle creation "category": "fuel", "amount": 15000.0, "date": date.today().isoformat() } async def clean_mailpit(): """Delete all messages in Mailpit before registration to ensure clean state.""" print(" [DEBUG] Entering clean_mailpit()") async with httpx.AsyncClient() as client: try: print(f" [DEBUG] Sending DELETE to {MAILPIT_DELETE_ALL}") resp = await client.delete(MAILPIT_DELETE_ALL) print(f" [DEBUG] DELETE response status: {resp.status_code}") if resp.status_code == 200: print("๐Ÿ—‘๏ธ Mailpit cleaned (all messages deleted).") else: print(f"โš ๏ธ Mailpit clean returned {resp.status_code}, continuing anyway.") except Exception as e: print(f"โš ๏ธ Mailpit clean failed: {e}, continuing anyway.") async def fetch_mailpit_token(): """Fetch the latest verification token from Mailpit with polling.""" import re import sys max_attempts = 5 wait_seconds = 3 print(f"[DEBUG] Starting fetch_mailpit_token() with max_attempts={max_attempts}", flush=True) async with httpx.AsyncClient() as client: for attempt in range(1, max_attempts + 1): try: print(f"[DEBUG] Fetching Mailpit messages (attempt {attempt}/{max_attempts})...", flush=True) resp = await client.get(MAILPIT_API) resp.raise_for_status() messages = resp.json() # Debug: print raw response summary total = messages.get("total", 0) count = messages.get("count", 0) print(f"[DEBUG] Mailpit response: total={total}, count={count}", flush=True) if not messages.get("messages"): print(f"โš ๏ธ No emails in Mailpit (attempt {attempt}/{max_attempts}). Waiting {wait_seconds}s...", flush=True) await asyncio.sleep(wait_seconds) continue # Print each message's subject and recipients for debugging for idx, msg in enumerate(messages.get("messages", [])): subject = msg.get("Subject", "No Subject") to_list = msg.get("To", []) from_list = msg.get("From", []) print(f"[DEBUG] Message {idx}: Subject='{subject}', To={to_list}, From={from_list}", flush=True) print(f"[DEBUG] Looking for email to {SANDBOX_EMAIL}...", flush=True) # Find the latest email to our sandbox email for msg in messages.get("messages", []): # Check if email is in To field (which is a list of dicts) to_list = msg.get("To", []) email_found = False for recipient in to_list: if isinstance(recipient, dict) and recipient.get("Address") == SANDBOX_EMAIL: email_found = True break elif isinstance(recipient, str) and recipient == SANDBOX_EMAIL: email_found = True break if email_found: msg_id = msg.get("ID") print(f"[DEBUG] Found email to {SANDBOX_EMAIL}, message ID: {msg_id}") # Fetch full message details (Text and HTML are empty in list response) if msg_id: try: # Correct endpoint: /api/v1/message/{id} (singular) detail_resp = await client.get(f"http://sf_mailpit:8025/api/v1/message/{msg_id}") detail_resp.raise_for_status() detail = detail_resp.json() body = detail.get("Text", "") html_body = detail.get("HTML", "") print(f"[DEBUG] Fetched full message details, body length: {len(body)}, HTML length: {len(html_body)}") except Exception as e: print(f"[DEBUG] Failed to fetch message details: {e}") body = msg.get("Text", "") html_body = msg.get("HTML", "") else: body = msg.get("Text", "") html_body = msg.get("HTML", "") if body: print(f"[DEBUG] Body preview (first 500 chars): {body[:500]}...") # Try to find token using patterns from test suite patterns = [ r"token=([a-zA-Z0-9\-_]+)", r"/verify/([a-zA-Z0-9\-_]+)", r"verification code: ([a-zA-Z0-9\-_]+)", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", # UUID pattern r"[0-9a-f]{32}", # UUID without hyphens ] for pattern in patterns: if body: token_match = re.search(pattern, body, re.I) if token_match: token = token_match.group(1) if token_match.groups() else token_match.group(0) print(f"โœ… Token found with pattern '{pattern}' on attempt {attempt}: {token}") return token # If not found in text, try HTML body if html_body: for pattern in patterns: html_token_match = re.search(pattern, html_body, re.I) if html_token_match: token = html_token_match.group(1) if html_token_match.groups() else html_token_match.group(0) print(f"โœ… Token found in HTML with pattern '{pattern}' on attempt {attempt}: {token}") return token print(f"[DEBUG] No token pattern found. Body length: {len(body)}, HTML length: {len(html_body)}") if body: print(f"[DEBUG] Full body (first 1000 chars): {body[:1000]}") if html_body: print(f"[DEBUG] HTML body snippet (first 500 chars): {html_body[:500]}") print(f"โš ๏ธ Email found but no token (attempt {attempt}/{max_attempts}). Waiting {wait_seconds}s...") await asyncio.sleep(wait_seconds) except Exception as e: print(f"โŒ Mailpit API error on attempt {attempt}: {e}") await asyncio.sleep(wait_seconds) print("โŒ Could not retrieve token after all attempts.") return None async def main(): print("๐Ÿš€ Starting Sandbox User Creation...") async with httpx.AsyncClient(base_url=API_BASE, timeout=30.0) as client: # Step 0: Clean Mailpit to ensure only new emails print("0. Cleaning Mailpit...") await clean_mailpit() # Step 1: Register print("1. Registering user...") register_data = { "email": SANDBOX_EMAIL, "password": SANDBOX_PASSWORD, "first_name": SANDBOX_FIRST_NAME, "last_name": SANDBOX_LAST_NAME, "region_code": "HU", "lang": "hu", "timezone": "Europe/Budapest" } resp = await client.post("/api/v1/auth/register", json=register_data) if resp.status_code not in (200, 201): print(f"โŒ Registration failed: {resp.status_code} {resp.text}") return print("โœ… Registration successful.") # Step 2: Get token from Mailpit print("2. Fetching verification token from Mailpit...") token = await fetch_mailpit_token() if not token: print("โŒ Could not retrieve token. Exiting.") return print(f"โœ… Token found: {token}") # Step 3: Verify email print("3. Verifying email...") resp = await client.post("/api/v1/auth/verify-email", json={"token": token}) if resp.status_code != 200: print(f"โŒ Email verification failed: {resp.status_code} {resp.text}") return print("โœ… Email verified.") # Step 4: Login print("4. Logging in...") resp = await client.post("/api/v1/auth/login", data={ "username": SANDBOX_EMAIL, "password": SANDBOX_PASSWORD }) if resp.status_code != 200: print(f"โŒ Login failed: {resp.status_code} {resp.text}") return login_data = resp.json() access_token = login_data.get("access_token") if not access_token: print("โŒ No access token in login response.") return print("โœ… Login successful.") # Update client headers with JWT client.headers.update({"Authorization": f"Bearer {access_token}"}) # Step 5: Complete KYC print("5. Completing KYC...") resp = await client.post("/api/v1/auth/complete-kyc", json=DUMMY_KYC) if resp.status_code != 200: print(f"โŒ KYC completion failed: {resp.status_code} {resp.text}") # Continue anyway (maybe KYC optional) else: print("โœ… KYC completed.") # Step 6: Create organization print("6. Creating organization...") resp = await client.post("/api/v1/organizations/onboard", json=DUMMY_ORG) if resp.status_code not in (200, 201): print(f"โŒ Organization creation failed: {resp.status_code} {resp.text}") # Continue anyway (maybe optional) org_id = None else: org_data = resp.json() org_id = org_data.get("organization_id") print(f"โœ… Organization created with ID: {org_id}") # Step 7: Add vehicle/asset print("7. Adding vehicle/asset...") asset_id = None # Try POST /api/v1/assets resp = await client.post("/api/v1/assets", json=DUMMY_VEHICLE) if resp.status_code in (200, 201): asset_data = resp.json() asset_id = asset_data.get("asset_id") or asset_data.get("id") print(f"โœ… Asset created via /api/v1/assets, ID: {asset_id}") else: # Try POST /api/v1/vehicles resp = await client.post("/api/v1/vehicles", json=DUMMY_VEHICLE) if resp.status_code in (200, 201): asset_data = resp.json() asset_id = asset_data.get("vehicle_id") or asset_data.get("id") print(f"โœ… Vehicle created via /api/v1/vehicles, ID: {asset_id}") else: # Try POST /api/v1/catalog/claim resp = await client.post("/api/v1/catalog/claim", json={ "catalog_id": DUMMY_VEHICLE["catalog_id"], "license_plate": DUMMY_VEHICLE["license_plate"] }) if resp.status_code in (200, 201): asset_data = resp.json() asset_id = asset_data.get("asset_id") or asset_data.get("id") print(f"โœ… Asset claimed via /api/v1/catalog/claim, ID: {asset_id}") else: print(f"โš ๏ธ Could not create vehicle/asset. Skipping. Status: {resp.status_code}, Response: {resp.text}") # Step 8: Add expense (if asset created) if asset_id: print("8. Adding expense (15,000 HUF fuel)...") expense_data = DUMMY_EXPENSE.copy() expense_data["asset_id"] = asset_id resp = await client.post("/api/v1/expenses/add", json=expense_data) if resp.status_code in (200, 201): print("โœ… Expense added.") else: print(f"โš ๏ธ Expense addition failed: {resp.status_code} {resp.text}") else: print("โš ๏ธ Skipping expense because no asset ID.") # Final output print("\n" + "="*60) print("๐ŸŽ‰ SANDBOX USER CREATION COMPLETE!") print("="*60) print(f"Email: {SANDBOX_EMAIL}") print(f"Password: {SANDBOX_PASSWORD}") print(f"JWT Access Token: {access_token}") print(f"Organization ID: {org_id}") print(f"Asset/Vehicle ID: {asset_id}") print(f"Login via Swagger: {API_BASE}/docs") print("="*60) print("\nYou can now use these credentials for manual testing.") print("Note: The user is fully verified and has a dummy organization,") print("a dummy vehicle, and a fuel expense of 15,000 HUF.") print("="*60) if __name__ == "__main__": asyncio.run(main())