átlagos kiegészítséek jó sok
This commit is contained in:
0
backend/app/tests/e2e/__init__.py
Normal file
0
backend/app/tests/e2e/__init__.py
Normal file
379
backend/app/tests/e2e/conftest.py
Normal file
379
backend/app/tests/e2e/conftest.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
Pytest fixtures for E2E testing of Core modules.
|
||||
Provides an authenticated client that goes through the full user journey.
|
||||
"""
|
||||
import asyncio
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import uuid
|
||||
import re
|
||||
import logging
|
||||
import time
|
||||
from typing import AsyncGenerator, Optional, List, Dict
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
from app.db.session import AsyncSessionLocal
|
||||
|
||||
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 delete_all_messages(self) -> bool:
|
||||
"""Delete all messages in Mailpit to ensure clean state."""
|
||||
try:
|
||||
response = await self.client.delete(f"{self.base_url}/api/v1/messages")
|
||||
response.raise_for_status()
|
||||
logger.debug("Mailpit cleaned (all messages deleted).")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Mailpit clean failed: {e}, continuing anyway.")
|
||||
return False
|
||||
|
||||
async def get_messages(self, limit: int = 50) -> Optional[Dict]:
|
||||
"""Fetch messages from Mailpit."""
|
||||
try:
|
||||
response = await self.client.get(f"{self.base_url}/api/v1/messages?limit={limit}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch messages: {e}")
|
||||
return None
|
||||
|
||||
async def get_latest_message(self) -> Optional[dict]:
|
||||
"""Fetch the latest email message from Mailpit."""
|
||||
data = await self.get_messages(limit=1)
|
||||
if data and data.get("messages"):
|
||||
return data["messages"][0]
|
||||
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 poll_for_verification_email(self, email: str, max_attempts: int = 5, wait_seconds: int = 3) -> Optional[str]:
|
||||
"""
|
||||
Poll Mailpit for a verification email sent to the given email address.
|
||||
Returns the verification token if found, otherwise None.
|
||||
"""
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
logger.debug(f"Polling for verification email (attempt {attempt}/{max_attempts})...")
|
||||
data = await self.get_messages(limit=20)
|
||||
if not data or not data.get("messages"):
|
||||
logger.debug(f"No emails in Mailpit, waiting {wait_seconds}s...")
|
||||
await asyncio.sleep(wait_seconds)
|
||||
continue
|
||||
|
||||
# Search for email sent to the target address
|
||||
for msg in data.get("messages", []):
|
||||
to_list = msg.get("To", [])
|
||||
email_found = False
|
||||
for recipient in to_list:
|
||||
if isinstance(recipient, dict) and recipient.get("Address") == email:
|
||||
email_found = True
|
||||
break
|
||||
elif isinstance(recipient, str) and recipient == email:
|
||||
email_found = True
|
||||
break
|
||||
|
||||
if email_found:
|
||||
msg_id = msg.get("ID")
|
||||
if not msg_id:
|
||||
continue
|
||||
content = await self.get_message_content(msg_id)
|
||||
if content:
|
||||
token = extract_verification_token(content)
|
||||
if token:
|
||||
logger.debug(f"Token found on attempt {attempt}: {token}")
|
||||
return token
|
||||
else:
|
||||
logger.debug(f"Email found but no token pattern matched.")
|
||||
else:
|
||||
logger.debug(f"Could not fetch email content.")
|
||||
|
||||
logger.debug(f"No verification email found yet, waiting {wait_seconds}s...")
|
||||
await asyncio.sleep(wait_seconds)
|
||||
|
||||
logger.error(f"Could not retrieve verification token after {max_attempts} attempts.")
|
||||
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:
|
||||
"""Verify email with token."""
|
||||
response = await self.client.post(
|
||||
f"{self.base_url}/api/v1/auth/verify-email",
|
||||
json={"token": token}
|
||||
)
|
||||
return response
|
||||
|
||||
async def complete_kyc(self, token: str) -> httpx.Response:
|
||||
"""Complete KYC with dummy data (matching Sandbox script)."""
|
||||
payload = {
|
||||
"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"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = await self.client.post(
|
||||
f"{self.base_url}/api/v1/auth/complete-kyc",
|
||||
json=payload,
|
||||
headers=headers
|
||||
)
|
||||
return response
|
||||
|
||||
async def get_authenticated_client(self, token: str) -> httpx.AsyncClient:
|
||||
"""Return a new httpx.AsyncClient with Authorization header set."""
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
return httpx.AsyncClient(base_url=self.base_url, headers=headers, timeout=30.0)
|
||||
|
||||
async def cleanup(self):
|
||||
"""Close the HTTP client."""
|
||||
await self.client.aclose()
|
||||
|
||||
|
||||
def extract_verification_token(email_content: str) -> Optional[str]:
|
||||
"""Extract verification token from email content."""
|
||||
# Look for token in URL patterns
|
||||
patterns = [
|
||||
r"token=([a-zA-Z0-9\-_]+)",
|
||||
r"/verify/([a-zA-Z0-9\-_]+)",
|
||||
r"verification code: ([a-zA-Z0-9\-_]+)",
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, email_content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""
|
||||
Database session fixture with automatic rollback after each test.
|
||||
This prevents InFailedSQLTransactionError between tests.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
# EZ A KULCS: Minden teszt után takarítunk!
|
||||
try:
|
||||
await session.rollback()
|
||||
except DBAPIError as e:
|
||||
logger.warning(f"Rollback failed (likely already aborted): {e}. Closing session.")
|
||||
await session.close()
|
||||
# Opcionálisan: await session.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def authenticated_client() -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
"""
|
||||
Fixture that performs full user journey and returns an authenticated httpx.AsyncClient.
|
||||
|
||||
Steps:
|
||||
1. Clean Mailpit to ensure only new emails
|
||||
2. Register a new user with unique email (time-based to avoid duplicate key)
|
||||
3. Poll Mailpit for verification email and extract token
|
||||
4. Verify email
|
||||
5. Login to get JWT token
|
||||
6. Complete KYC
|
||||
7. Return authenticated client with Authorization header
|
||||
"""
|
||||
# Generate unique email using timestamp to avoid duplicate key errors in user_stats
|
||||
unique_id = int(time.time() * 1000) # milliseconds
|
||||
email = f"test_{unique_id}@{TEST_EMAIL_DOMAIN}"
|
||||
password = "TestPassword123!"
|
||||
|
||||
api_client = APIClient()
|
||||
mailpit = MailpitClient()
|
||||
|
||||
try:
|
||||
# 0. Clean Mailpit before registration
|
||||
logger.debug("Cleaning Mailpit before registration...")
|
||||
await mailpit.delete_all_messages()
|
||||
|
||||
# 1. Register
|
||||
logger.debug(f"Registering user with email: {email}")
|
||||
reg_response = await api_client.register(email, password)
|
||||
assert reg_response.status_code in (200, 201), f"Registration failed: {reg_response.text}"
|
||||
|
||||
# 2. Poll for verification email and extract token
|
||||
logger.debug("Polling Mailpit for verification email...")
|
||||
token = await mailpit.poll_for_verification_email(email, max_attempts=5, wait_seconds=3)
|
||||
assert token is not None, "Could not retrieve verification token after polling"
|
||||
|
||||
# 3. Verify email
|
||||
verify_response = await api_client.verify_email(token)
|
||||
assert verify_response.status_code == 200, f"Email verification failed: {verify_response.text}"
|
||||
|
||||
# 4. Login
|
||||
access_token = await api_client.login(email, password)
|
||||
assert access_token is not None, "Login failed"
|
||||
|
||||
# 5. Complete KYC (optional, log failure but continue)
|
||||
kyc_response = await api_client.complete_kyc(access_token)
|
||||
if kyc_response.status_code != 200:
|
||||
logger.warning(f"KYC completion returned {kyc_response.status_code}: {kyc_response.text}. Continuing anyway.")
|
||||
|
||||
# 6. Create authenticated client
|
||||
auth_client = await api_client.get_authenticated_client(access_token)
|
||||
yield auth_client
|
||||
|
||||
# Cleanup
|
||||
await auth_client.aclose()
|
||||
|
||||
finally:
|
||||
await api_client.cleanup()
|
||||
await mailpit.cleanup()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def setup_organization(authenticated_client):
|
||||
"""Létrehoz egy céget a jármű/költség tesztekhez."""
|
||||
import time
|
||||
import random
|
||||
unique_id = int(time.time() * 1000) + random.randint(1, 9999)
|
||||
# Generate a valid Hungarian tax number format: 8 digits + "-1-42"
|
||||
tax_prefix = random.randint(10000000, 99999999)
|
||||
payload = {
|
||||
"name": f"Test Fleet {unique_id}",
|
||||
"display_name": f"Test Fleet {unique_id}",
|
||||
"full_name": f"Test Fleet Kft. {unique_id}",
|
||||
"tax_number": f"{tax_prefix}-1-42",
|
||||
"registration_number": f"01-09-{unique_id}"[:6],
|
||||
"org_type": "business",
|
||||
"country_code": "HU",
|
||||
"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,
|
||||
}
|
||||
response = await authenticated_client.post("/api/v1/organizations/onboard", json=payload)
|
||||
# Accept 409 as well (already exists) and try to fetch existing organization
|
||||
if response.status_code == 409:
|
||||
# Maybe we can reuse the existing organization? For simplicity, we'll just skip and raise.
|
||||
# But we need an organization ID. Let's try to get the user's organizations.
|
||||
# For now, raise a specific error.
|
||||
raise ValueError(f"Organization with tax number already exists: {payload['tax_number']}")
|
||||
assert response.status_code in (200, 201), f"Organization creation failed: {response.text}"
|
||||
data = response.json()
|
||||
# Try multiple possible keys for organization ID
|
||||
if "id" in data:
|
||||
return data["id"]
|
||||
elif "organization_id" in data:
|
||||
return data["organization_id"]
|
||||
elif "organization" in data and "id" in data["organization"]:
|
||||
return data["organization"]["id"]
|
||||
else:
|
||||
# Fallback: extract from location header or raise
|
||||
raise ValueError(f"Could not find organization ID in response: {data}")
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def setup_vehicle(authenticated_client, setup_organization):
|
||||
import time
|
||||
unique_vin = f"WBA0000000{int(time.time())}"[:17].ljust(17, '0')
|
||||
payload = {
|
||||
"vin": unique_vin,
|
||||
"license_plate": "TEST-123",
|
||||
"organization_id": setup_organization,
|
||||
"purchase_price_net": 10000000,
|
||||
"purchase_date": "2023-01-01",
|
||||
"initial_mileage": 10000,
|
||||
"fuel_type": "petrol",
|
||||
"transmission": "manual"
|
||||
}
|
||||
response = await authenticated_client.post("/api/v1/assets/vehicles", json=payload)
|
||||
assert response.status_code in (200, 201), f"Vehicle creation failed: {response.text}"
|
||||
return response.json()["id"]
|
||||
85
backend/app/tests/e2e/test_admin_security.py
Normal file
85
backend/app/tests/e2e/test_admin_security.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
E2E teszt az admin végpontok biztonsági ellenőrzéséhez.
|
||||
Ellenőrzi, hogy normál felhasználó nem fér hozzá admin végponthoz.
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
from app.models.identity import User, UserRole
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
|
||||
def test_normal_user_cannot_access_admin_ping():
|
||||
"""
|
||||
Normál felhasználó nem fér hozzá a GET /api/v1/admin/ping végponthoz.
|
||||
Elvárt: 403 Forbidden.
|
||||
"""
|
||||
# Mock a normal user (non-admin)
|
||||
mock_user = User(
|
||||
id=999,
|
||||
email="normal@example.com",
|
||||
role=UserRole.user,
|
||||
is_active=True,
|
||||
is_deleted=False,
|
||||
subscription_plan="FREE",
|
||||
preferred_language="hu",
|
||||
region_code="HU",
|
||||
preferred_currency="HUF",
|
||||
scope_level="individual",
|
||||
custom_permissions={}
|
||||
)
|
||||
|
||||
# Override get_current_user to return normal user
|
||||
async def mock_get_current_user():
|
||||
return mock_user
|
||||
|
||||
app.dependency_overrides[get_current_user] = mock_get_current_user
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/v1/admin/ping")
|
||||
|
||||
# Clean up
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 403
|
||||
assert "detail" in response.json()
|
||||
print(f"Response detail: {response.json()['detail']}")
|
||||
|
||||
|
||||
def test_admin_user_can_access_admin_ping():
|
||||
"""
|
||||
Admin felhasználóval a ping végpont 200-at ad vissza.
|
||||
"""
|
||||
mock_admin = User(
|
||||
id=1000,
|
||||
email="admin@example.com",
|
||||
role=UserRole.admin,
|
||||
is_active=True,
|
||||
is_deleted=False,
|
||||
subscription_plan="PREMIUM",
|
||||
preferred_language="en",
|
||||
region_code="HU",
|
||||
preferred_currency="EUR",
|
||||
scope_level="global",
|
||||
custom_permissions={}
|
||||
)
|
||||
|
||||
async def mock_get_current_user():
|
||||
return mock_admin
|
||||
|
||||
app.dependency_overrides[get_current_user] = mock_get_current_user
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/v1/admin/ping")
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "Admin felület aktív"
|
||||
assert data["role"] == "admin"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
28
backend/app/tests/e2e/test_analytics_import.py
Normal file
28
backend/app/tests/e2e/test_analytics_import.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick test to verify analytics module imports correctly.
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, '/opt/docker/dev/service_finder/backend')
|
||||
|
||||
try:
|
||||
from app.api.v1.endpoints.analytics import router
|
||||
print("✓ Analytics router imported successfully")
|
||||
print(f"Router prefix: {router.prefix}")
|
||||
print(f"Router tags: {router.tags}")
|
||||
except ImportError as e:
|
||||
print(f"✗ Import error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"✗ Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Try importing schemas
|
||||
try:
|
||||
from app.schemas.analytics import TCOSummaryResponse
|
||||
print("✓ Analytics schemas imported successfully")
|
||||
except ImportError as e:
|
||||
print(f"✗ Schemas import error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("All imports passed.")
|
||||
37
backend/app/tests/e2e/test_expense_flow.py
Normal file
37
backend/app/tests/e2e/test_expense_flow.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
End-to-end test for Expense creation flow.
|
||||
Uses the authenticated_client fixture to test POST /api/v1/expenses/add endpoint.
|
||||
"""
|
||||
import pytest
|
||||
import httpx
|
||||
from datetime import date
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expense_creation(authenticated_client: httpx.AsyncClient, setup_vehicle):
|
||||
"""
|
||||
Test that a user can add an expense (fuel/service) to an asset.
|
||||
Uses the setup_vehicle fixture to get a valid asset_id.
|
||||
"""
|
||||
asset_id = setup_vehicle
|
||||
|
||||
# Now add an expense for this asset
|
||||
expense_payload = {
|
||||
"asset_id": str(asset_id), # must be string
|
||||
"category": "fuel", # or "service", "insurance", etc.
|
||||
"amount": 15000.0,
|
||||
"date": str(date.today()), # YYYY-MM-DD
|
||||
}
|
||||
|
||||
response = await authenticated_client.post(
|
||||
"/api/v1/expenses/add",
|
||||
json=expense_payload
|
||||
)
|
||||
|
||||
# Assert success
|
||||
assert response.status_code == 200, f"Unexpected status: {response.status_code}, response: {response.text}"
|
||||
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
|
||||
print(f"✅ Expense added for asset {asset_id}")
|
||||
119
backend/app/tests/e2e/test_hierarchical_params.py
Normal file
119
backend/app/tests/e2e/test_hierarchical_params.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Teszt szkript a hierarchikus System Parameters működésének ellenőrzéséhez.
|
||||
Futtatás: docker exec sf_api python /app/test_hierarchical_params.py
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.models.system import SystemParameter, ParameterScope
|
||||
from app.services.system_service import system_service
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@postgres:5432/service_finder")
|
||||
|
||||
async def test_hierarchical():
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
# Töröljük a teszt paramétereket, ha vannak
|
||||
await db.execute(
|
||||
SystemParameter.__table__.delete().where(SystemParameter.key == "test.hierarchical")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# 1. GLOBAL paraméter létrehozása
|
||||
global_param = SystemParameter(
|
||||
key="test.hierarchical",
|
||||
value={"message": "global value"},
|
||||
scope_level=ParameterScope.GLOBAL,
|
||||
scope_id=None,
|
||||
category="test",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(global_param)
|
||||
|
||||
# 2. COUNTRY paraméter létrehozása (HU)
|
||||
country_param = SystemParameter(
|
||||
key="test.hierarchical",
|
||||
value={"message": "country HU value"},
|
||||
scope_level=ParameterScope.COUNTRY,
|
||||
scope_id="HU",
|
||||
category="test",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(country_param)
|
||||
|
||||
# 3. REGION paraméter létrehozása (budapest)
|
||||
region_param = SystemParameter(
|
||||
key="test.hierarchical",
|
||||
value={"message": "region budapest value"},
|
||||
scope_level=ParameterScope.REGION,
|
||||
scope_id="budapest",
|
||||
category="test",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(region_param)
|
||||
|
||||
# 4. USER paraméter létrehozása (user_123)
|
||||
user_param = SystemParameter(
|
||||
key="test.hierarchical",
|
||||
value={"message": "user user_123 value"},
|
||||
scope_level=ParameterScope.USER,
|
||||
scope_id="user_123",
|
||||
category="test",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user_param)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Teszt: csak global scope (nincs user, region, country)
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", default=None)
|
||||
print(f"Global only: {value}")
|
||||
assert value["message"] == "global value"
|
||||
|
||||
# COUNTRY scope (HU)
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", country_code="HU", default=None)
|
||||
print(f"Country HU: {value}")
|
||||
assert value["message"] == "country HU value"
|
||||
|
||||
# REGION scope (budapest) – a region a country feletti prioritás? A prioritási sorrend: User > Region > Country > Global
|
||||
# Ha region_id megadva, de country_code is, akkor region elsőbbséget élvez.
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", region_id="budapest", country_code="HU", default=None)
|
||||
print(f"Region budapest (with country HU): {value}")
|
||||
assert value["message"] == "region budapest value"
|
||||
|
||||
# USER scope (user_123) – legmagasabb prioritás
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", user_id="user_123", region_id="budapest", country_code="HU", default=None)
|
||||
print(f"User user_123 (with region and country): {value}")
|
||||
assert value["message"] == "user user_123 value"
|
||||
|
||||
# Nem létező user, de létező region
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", user_id="nonexistent", region_id="budapest", country_code="HU", default=None)
|
||||
print(f"Non-existent user, region budapest: {value}")
|
||||
assert value["message"] == "region budapest value"
|
||||
|
||||
# Nem létező region, de létező country
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", region_id="nonexistent", country_code="HU", default=None)
|
||||
print(f"Non-existent region, country HU: {value}")
|
||||
assert value["message"] == "country HU value"
|
||||
|
||||
# Semmi specifikus – global
|
||||
value = await system_service.get_scoped_parameter(db, "test.hierarchical", default=None)
|
||||
print(f"Fallback to global: {value}")
|
||||
assert value["message"] == "global value"
|
||||
|
||||
# Törlés
|
||||
await db.execute(
|
||||
SystemParameter.__table__.delete().where(SystemParameter.key == "test.hierarchical")
|
||||
)
|
||||
await db.commit()
|
||||
print("✅ Minden teszt sikeres!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_hierarchical())
|
||||
72
backend/app/tests/e2e/test_marketplace_flow.py
Normal file
72
backend/app/tests/e2e/test_marketplace_flow.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
End-to-end test for Service Hunt (Marketplace) flow.
|
||||
Tests the POST /api/v1/services/hunt endpoint with form data.
|
||||
"""
|
||||
import pytest
|
||||
import httpx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_hunt(authenticated_client: httpx.AsyncClient):
|
||||
"""
|
||||
Test that a user can submit a service hunt (discovery) with location data.
|
||||
"""
|
||||
# Payload as form data (x-www-form-urlencoded)
|
||||
payload = {
|
||||
"name": "Test Garage",
|
||||
"lat": 47.4979,
|
||||
"lng": 19.0402
|
||||
}
|
||||
|
||||
# Note: httpx sends form data with data=, not json=
|
||||
response = await authenticated_client.post(
|
||||
"/api/v1/services/hunt",
|
||||
data=payload
|
||||
)
|
||||
|
||||
# Assert success
|
||||
assert response.status_code == 200, f"Unexpected status: {response.status_code}, response: {response.text}"
|
||||
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
|
||||
print(f"✅ Service hunt submitted successfully: {data}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_validation(authenticated_client: httpx.AsyncClient):
|
||||
"""
|
||||
Test the validation endpoint for staged service records.
|
||||
- Creates a staging record via hunt endpoint.
|
||||
- Attempts to validate own submission (should fail with 400).
|
||||
- (Optional) Successful validation by a different user would require a second user.
|
||||
"""
|
||||
# 1. Create a staging record
|
||||
payload = {
|
||||
"name": "Validation Test Garage",
|
||||
"lat": 47.5000,
|
||||
"lng": 19.0500
|
||||
}
|
||||
response = await authenticated_client.post(
|
||||
"/api/v1/services/hunt",
|
||||
data=payload
|
||||
)
|
||||
assert response.status_code == 200, f"Failed to create staging: {response.text}"
|
||||
hunt_data = response.json()
|
||||
staging_id = hunt_data.get("staging_id")
|
||||
if not staging_id:
|
||||
# If response doesn't contain staging_id, we need to extract it from the message
|
||||
# For now, skip this test if staging_id not present
|
||||
print("⚠️ staging_id not found in response, skipping validation test")
|
||||
return
|
||||
|
||||
# 2. Attempt to validate own submission (should return 400)
|
||||
validate_response = await authenticated_client.post(
|
||||
f"/api/v1/services/hunt/{staging_id}/validate"
|
||||
)
|
||||
# Expect 400 Bad Request because user cannot validate their own submission
|
||||
assert validate_response.status_code == 400, f"Expected 400 for self-validation, got {validate_response.status_code}: {validate_response.text}"
|
||||
|
||||
# 3. (Optional) Successful validation by a different user would require a second authenticated client.
|
||||
# For now, we can at least verify that the endpoint exists and returns proper error.
|
||||
print(f"✅ Self-validation correctly rejected with 400")
|
||||
58
backend/app/tests/e2e/test_organization_flow.py
Normal file
58
backend/app/tests/e2e/test_organization_flow.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
End-to-end test for Organization onboarding flow.
|
||||
Uses the authenticated_client fixture to test the POST /api/v1/organizations/onboard endpoint.
|
||||
"""
|
||||
import pytest
|
||||
import httpx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_organization_onboard(authenticated_client: httpx.AsyncClient):
|
||||
"""
|
||||
Test that a user can create an organization via the onboard endpoint.
|
||||
"""
|
||||
# Prepare payload according to CorpOnboardIn schema
|
||||
payload = {
|
||||
"full_name": "Test Corporation Kft.",
|
||||
"name": "TestCorp",
|
||||
"display_name": "Test Corporation",
|
||||
"tax_number": "12345678-2-41",
|
||||
"reg_number": "01-09-123456",
|
||||
"country_code": "HU",
|
||||
"language": "hu",
|
||||
"default_currency": "HUF",
|
||||
# Atomic address fields
|
||||
"address_zip": "1234",
|
||||
"address_city": "Budapest",
|
||||
"address_street_name": "Test",
|
||||
"address_street_type": "utca",
|
||||
"address_house_number": "1",
|
||||
"address_stairwell": "A",
|
||||
"address_floor": "2",
|
||||
"address_door": "3",
|
||||
# Optional contacts
|
||||
"contacts": [
|
||||
{
|
||||
"full_name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"phone": "+36123456789",
|
||||
"contact_type": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = await authenticated_client.post(
|
||||
"/api/v1/organizations/onboard",
|
||||
json=payload
|
||||
)
|
||||
|
||||
# Assert success (201 Created or 200 OK)
|
||||
assert response.status_code in (200, 201), f"Unexpected status: {response.status_code}, response: {response.text}"
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
assert "organization_id" in data
|
||||
assert data["organization_id"] > 0
|
||||
assert data["status"] == "pending_verification"
|
||||
|
||||
print(f"✅ Organization created with ID: {data['organization_id']}")
|
||||
74
backend/app/tests/e2e/test_r0_spider.py
Normal file
74
backend/app/tests/e2e/test_r0_spider.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Teszt szkript az R0 spider számára.
|
||||
Csak egy járművet dolgoz fel, majd leáll.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from app.workers.vehicle.ultimatespecs.vehicle_ultimate_r0_spider import UltimateSpecsSpider
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [TEST-R0] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger("TEST-R0")
|
||||
|
||||
class TestSpider(UltimateSpecsSpider):
|
||||
"""Teszt spider, amely csak egy iterációt fut."""
|
||||
|
||||
async def run_test(self):
|
||||
"""Run a single test iteration."""
|
||||
logger.info("Teszt spider indítása...")
|
||||
|
||||
try:
|
||||
await self.init_browser()
|
||||
|
||||
# Process just one vehicle
|
||||
processed = await self.process_single_vehicle()
|
||||
|
||||
if processed:
|
||||
logger.info("Teszt sikeres - egy jármű feldolgozva")
|
||||
else:
|
||||
logger.info("Teszt sikeres - nincs feldolgozandó jármű")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Teszt hiba: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
await self.close_browser()
|
||||
|
||||
return True
|
||||
|
||||
async def main():
|
||||
"""Main test function."""
|
||||
spider = TestSpider()
|
||||
|
||||
try:
|
||||
success = await spider.run_test()
|
||||
if success:
|
||||
print("\n✅ TESZT SIKERES")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n❌ TESZT SIKERTELEN")
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏹️ Teszt megszakítva")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n💥 Váratlan hiba: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
23
backend/app/tests/e2e/test_robot.py
Executable file
23
backend/app/tests/e2e/test_robot.py
Executable file
@@ -0,0 +1,23 @@
|
||||
# Tell pytest to skip this module - it's a standalone script, not a test
|
||||
__test__ = False
|
||||
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.services.harvester_robot import VehicleHarvester
|
||||
from app.core.config import settings
|
||||
|
||||
# Adatbázis kapcsolat felépítése a pontos névvel
|
||||
engine = create_async_engine(str(settings.DATABASE_URL))
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async def run_test():
|
||||
async with AsyncSessionLocal() as db:
|
||||
harvester = VehicleHarvester()
|
||||
print("🚀 Robot indítása...")
|
||||
# Megpróbáljuk betölteni a katalógust
|
||||
await harvester.harvest_all(db)
|
||||
print("✅ Teszt lefutott.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_test())
|
||||
85
backend/app/tests/e2e/test_trust_endpoint.py
Normal file
85
backend/app/tests/e2e/test_trust_endpoint.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Egyszerű teszt a Gondos Gazda Index API végponthoz.
|
||||
"""
|
||||
|
||||
# Tell pytest to skip this module - it's a standalone script, not a test
|
||||
__test__ = False
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.services.trust_engine import TrustEngine
|
||||
from app.models.identity import User
|
||||
|
||||
async def test_trust_engine():
|
||||
"""Teszteli a TrustEngine működését."""
|
||||
print("TrustEngine teszt indítása...")
|
||||
|
||||
# Adatbázis kapcsolat
|
||||
engine = create_async_engine(
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/service_finder",
|
||||
echo=False
|
||||
)
|
||||
|
||||
async_session = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as db:
|
||||
# Keressünk egy teszt felhasználót
|
||||
from sqlalchemy import select
|
||||
stmt = select(User).limit(1)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
print("Nincs felhasználó az adatbázisban, teszt felhasználó létrehozása...")
|
||||
# Egyszerűsítés: csak kiírjuk, hogy nincs felhasználó
|
||||
print("Nincs felhasználó, a teszt kihagyva.")
|
||||
return
|
||||
|
||||
print(f"Teszt felhasználó: {user.email} (ID: {user.id})")
|
||||
|
||||
# TrustEngine példányosítás
|
||||
trust_engine = TrustEngine()
|
||||
|
||||
# Trust számítás
|
||||
trust_data = await trust_engine.calculate_user_trust(db, user.id)
|
||||
|
||||
print("\n=== Trust Score Eredmény ===")
|
||||
print(f"Trust Score: {trust_data['trust_score']}/100")
|
||||
print(f"Maintenance Score: {trust_data['maintenance_score']:.2f}")
|
||||
print(f"Quality Score: {trust_data['quality_score']:.2f}")
|
||||
print(f"Preventive Score: {trust_data['preventive_score']:.2f}")
|
||||
print(f"Last Calculated: {trust_data['last_calculated']}")
|
||||
|
||||
if trust_data['weights']:
|
||||
print(f"\nSúlyozások:")
|
||||
for key, value in trust_data['weights'].items():
|
||||
print(f" {key}: {value:.2f}")
|
||||
|
||||
if trust_data['tolerance_km']:
|
||||
print(f"Tolerancia KM: {trust_data['tolerance_km']}")
|
||||
|
||||
# Ellenőrizzük, hogy a UserTrustProfile létrejött-e
|
||||
from sqlalchemy import select
|
||||
from app.models.identity import UserTrustProfile
|
||||
stmt = select(UserTrustProfile).where(UserTrustProfile.user_id == user.id)
|
||||
result = await db.execute(stmt)
|
||||
profile = result.scalar_one_or_none()
|
||||
|
||||
if profile:
|
||||
print(f"\nUserTrustProfile létrehozva:")
|
||||
print(f" Trust Score: {profile.trust_score}")
|
||||
print(f" Last Calculated: {profile.last_calculated}")
|
||||
else:
|
||||
print("\nFIGYELEM: UserTrustProfile nem jött létre!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_trust_engine())
|
||||
101
backend/app/tests/e2e/test_trust_endpoint_simple.py
Normal file
101
backend/app/tests/e2e/test_trust_endpoint_simple.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Egyszerű teszt a Gondos Gazda Index API végponthoz - import hibák elkerülésével.
|
||||
"""
|
||||
|
||||
# Tell pytest to skip this module - it's a standalone script, not a test
|
||||
__test__ = False
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ideiglenes megoldás: mockoljuk a hiányzó importokat
|
||||
import unittest.mock as mock
|
||||
|
||||
# Mock the missing imports before importing trust_engine
|
||||
sys.modules['app.models.asset'] = mock.Mock()
|
||||
sys.modules['app.models.service'] = mock.Mock()
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.services.trust_engine import TrustEngine
|
||||
from app.models.identity import User
|
||||
|
||||
async def test_trust_engine():
|
||||
"""Teszteli a TrustEngine működését."""
|
||||
print("TrustEngine teszt indítása...")
|
||||
|
||||
# Adatbázis kapcsolat
|
||||
engine = create_async_engine(
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/service_finder",
|
||||
echo=False
|
||||
)
|
||||
|
||||
async_session = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as db:
|
||||
# Keressünk egy teszt felhasználót
|
||||
from sqlalchemy import select
|
||||
stmt = select(User).limit(1)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
print("Nincs felhasználó az adatbázisban, teszt felhasználó létrehozása...")
|
||||
# Egyszerűsítés: csak kiírjuk, hogy nincs felhasználó
|
||||
print("Nincs felhasználó, a teszt kihagyva.")
|
||||
return
|
||||
|
||||
print(f"Teszt felhasználó: {user.email} (ID: {user.id})")
|
||||
|
||||
# TrustEngine példányosítás
|
||||
trust_engine = TrustEngine()
|
||||
|
||||
# Trust számítás (force_recalculate=True, hogy biztosan számoljon)
|
||||
try:
|
||||
trust_data = await trust_engine.calculate_user_trust(db, user.id, force_recalculate=True)
|
||||
|
||||
print("\n=== Trust Score Eredmény ===")
|
||||
print(f"Trust Score: {trust_data['trust_score']}/100")
|
||||
print(f"Maintenance Score: {trust_data['maintenance_score']:.2f}")
|
||||
print(f"Quality Score: {trust_data['quality_score']:.2f}")
|
||||
print(f"Preventive Score: {trust_data['preventive_score']:.2f}")
|
||||
print(f"Last Calculated: {trust_data['last_calculated']}")
|
||||
|
||||
if trust_data['weights']:
|
||||
print(f"\nSúlyozások:")
|
||||
for key, value in trust_data['weights'].items():
|
||||
print(f" {key}: {value:.2f}")
|
||||
|
||||
if trust_data['tolerance_km']:
|
||||
print(f"Tolerancia KM: {trust_data['tolerance_km']}")
|
||||
|
||||
# Ellenőrizzük, hogy a UserTrustProfile létrejött-e
|
||||
from sqlalchemy import select
|
||||
from app.models.identity import UserTrustProfile
|
||||
stmt = select(UserTrustProfile).where(UserTrustProfile.user_id == user.id)
|
||||
result = await db.execute(stmt)
|
||||
profile = result.scalar_one_or_none()
|
||||
|
||||
if profile:
|
||||
print(f"\nUserTrustProfile létrehozva:")
|
||||
print(f" Trust Score: {profile.trust_score}")
|
||||
print(f" Last Calculated: {profile.last_calculated}")
|
||||
else:
|
||||
print("\nFIGYELEM: UserTrustProfile nem jött létre!")
|
||||
|
||||
print("\n✅ TrustEngine sikeresen működik!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Hiba történt: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_trust_engine())
|
||||
256
backend/app/tests/e2e/test_user_registration_flow.py
Normal file
256
backend/app/tests/e2e/test_user_registration_flow.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
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())
|
||||
49
backend/app/tests/e2e/test_vehicle_flow.py
Normal file
49
backend/app/tests/e2e/test_vehicle_flow.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
End-to-end test for Vehicle/Asset creation flow.
|
||||
Uses the authenticated_client fixture to test adding a new vehicle to the user's garage.
|
||||
"""
|
||||
import pytest
|
||||
import httpx
|
||||
import uuid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vehicle_creation(authenticated_client: httpx.AsyncClient, setup_organization):
|
||||
"""
|
||||
Test that a user can add a new vehicle (asset) to their garage.
|
||||
Uses the new POST /api/v1/assets/vehicles endpoint.
|
||||
"""
|
||||
# Generate unique VIN and license plate
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
# VIN must be exactly 17 characters
|
||||
vin = f"VIN{unique_suffix}123456" # 3 + 8 + 6 = 17
|
||||
payload = {
|
||||
"vin": vin,
|
||||
"license_plate": f"TEST-{unique_suffix[:6]}",
|
||||
# catalog_id omitted (optional)
|
||||
"organization_id": setup_organization,
|
||||
}
|
||||
# The backend will uppercase the VIN, so we compare case-insensitively
|
||||
expected_vin = vin.upper()
|
||||
|
||||
# POST to the new endpoint
|
||||
response = await authenticated_client.post(
|
||||
"/api/v1/assets/vehicles",
|
||||
json=payload
|
||||
)
|
||||
|
||||
# Assert success (201 Created)
|
||||
assert response.status_code == 201, f"Unexpected status: {response.status_code}, response: {response.text}"
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
# Expect AssetResponse schema
|
||||
assert "id" in data
|
||||
assert data["vin"] == expected_vin
|
||||
assert data["license_plate"] == payload["license_plate"].upper()
|
||||
|
||||
asset_id = data["id"]
|
||||
print(f"✅ Vehicle/Asset created with ID: {asset_id}")
|
||||
|
||||
# Return the asset_id for potential use in expense test
|
||||
return asset_id
|
||||
Reference in New Issue
Block a user