teszt állományok áthelyezése és szelektálása

This commit is contained in:
Roo
2026-06-05 10:50:25 +00:00
parent 18524a08f2
commit f03b5f3916
63 changed files with 633 additions and 527 deletions

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Create integration_session.json with test identity credentials.
Run with: docker compose exec sf_api python /app/create_integration_session.py
"""
import asyncio
import sys
import json
import os
from datetime import datetime, timezone
import uuid
sys.path.insert(0, '/app')
async def main():
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.models.marketplace.organization import OrganizationMember
from app.models import Asset, VehicleModelDefinition
from app.services.auth_service import AuthService
from app.core.security import create_tokens, get_password_hash
from app.core.config import settings
from sqlalchemy import select
TEST_EMAIL = "tester_pro@profibot.hu"
TEST_PASSWORD = "TestPassword123!"
async with AsyncSessionLocal() as db:
# Get the admin user
result = await db.execute(select(User).where(User.email == TEST_EMAIL))
user = result.scalar_one_or_none()
if not user:
print(f"User {TEST_EMAIL} not found, creating...")
# We would need to create user, but skip for now
print("Cannot proceed")
return
print(f"Found user: {user.email}, ID: {user.id}, Role: {user.role}")
# Ensure password is set
if not user.hashed_password or not await AuthService.authenticate(db, TEST_EMAIL, TEST_PASSWORD):
user.hashed_password = get_password_hash(TEST_PASSWORD)
await db.commit()
print("Password updated")
# Generate token
auth_user = await AuthService.authenticate(db, TEST_EMAIL, TEST_PASSWORD)
if not auth_user:
print("Authentication failed after password update")
return
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default={})
role_key = auth_user.role.value.upper()
token_payload = {
"sub": str(auth_user.id),
"role": auth_user.role.value,
"rank": ranks.get(role_key, 10),
"scope_level": auth_user.scope_level or "individual",
"scope_id": str(auth_user.scope_id) if auth_user.scope_id else str(auth_user.id)
}
access_token, refresh_token = create_tokens(data=token_payload)
print(f"Token generated: {access_token[:50]}...")
# Get organization ID if any
result = await db.execute(
select(OrganizationMember.organization_id)
.where(OrganizationMember.user_id == user.id)
.limit(1)
)
org_member = result.scalar_one_or_none()
org_id = org_member.organization_id if org_member else None
# Get a test vehicle ID
result = await db.execute(
select(Asset.id)
.where(Asset.owner_user_id == user.id)
.limit(1)
)
vehicle = result.scalar_one_or_none()
vehicle_id = vehicle.id if vehicle else None
# If no vehicle, create one
if not vehicle_id:
result = await db.execute(select(VehicleModelDefinition.id).limit(1))
catalog_id = result.scalar_one_or_none()
if catalog_id:
vehicle = Asset(
catalog_id=catalog_id,
license_plate=f"TEST-{uuid.uuid4().hex[:4]}".upper(),
vin=f"VIN{uuid.uuid4().hex[:10]}".upper(),
nickname="Integration Test Vehicle",
owner_user_id=user.id,
status="DRAFT",
created_at=datetime.now(timezone.utc)
)
db.add(vehicle)
await db.commit()
await db.refresh(vehicle)
vehicle_id = vehicle.id
print(f"Created test vehicle with ID {vehicle_id}")
else:
print("No catalog entries found, skipping vehicle creation")
# Prepare session data
session_data = {
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"test_token": access_token,
"user_id": user.id,
"role": user.role.value,
"organization_id": org_id,
"test_vehicle_id": vehicle_id
}
# Write to file
output_path = "/opt/docker/dev/service_finder/tests/integration_session.json"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w') as f:
json.dump(session_data, f, indent=2)
print("\n" + "="*60)
print("TEST IDENTITY SETUP COMPLETE")
print("="*60)
print(f"Email: {session_data['email']}")
print(f"Password: {session_data['password']}")
print(f"Token: {session_data['test_token'][:50]}...")
print(f"User ID: {session_data['user_id']}")
print(f"Role: {session_data['role']}")
print(f"Organization ID: {session_data['organization_id']}")
print(f"Test Vehicle ID: {session_data['test_vehicle_id']}")
print(f"Session saved to: {output_path}")
print("="*60)
return session_data
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""
Playwright E2E Test: Profile Address Persistence (Stairwell / Floor / Door)
This test:
1. Logs in as the configured test user (via LoginModal on the landing page).
2. Navigates to the Profile page (/profile).
3. Enters edit mode, fills every address field (city, zip, street, house number,
stairwell, floor, door), and saves.
4. Reloads the page and verifies that the stairwell, floor, and door values
persisted after the page refresh.
Requirements:
- playwright (Python) + dotenv
- .env.test file at the project root with TEST_USER_EMAIL and TEST_USER_PASS
Usage:
python tests/active/e2e_profile_address.py
To see the browser visually, change HEADLESS=False below.
"""
import os
import sys
import re
from pathlib import Path
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
# ── Configuration ──────────────────────────────────────────────────────────
BASE_URL = "https://app.servicefinder.hu"
HEADLESS = True # Set to False for visual debugging
SLOW_MO = 0 # ms delay between actions (useful for debugging)
ACTION_TIMEOUT = 15_000 # ms default timeout for element lookups
# ── Load .env.test from project root ──────────────────────────────────────
ENV_PATH = Path(__file__).resolve().parents[1] / ".env.test"
if not ENV_PATH.exists():
raise FileNotFoundError(
f".env.test not found at {ENV_PATH}. "
"Create it with TEST_USER_EMAIL and TEST_USER_PASS."
)
load_dotenv(dotenv_path=ENV_PATH, override=True)
TEST_EMAIL = os.getenv("TEST_USER_EMAIL")
TEST_PASS = os.getenv("TEST_USER_PASS")
if not TEST_EMAIL or not TEST_PASS:
raise ValueError(
"TEST_USER_EMAIL and/or TEST_USER_PASS are missing from .env.test. "
f"Loaded from: {ENV_PATH}"
)
# ── Test data ──────────────────────────────────────────────────────────────
ADDRESS_DATA = {
"address_city": "Dunakeszi",
"address_zip": "2120",
"address_street_name": "Határ",
"address_street_type": "utca",
"address_house_number": "8",
"address_stairwell": "A",
"address_floor": "3",
"address_door": "5",
}
# ═══════════════════════════════════════════════════════════════════════════
# Helper functions
# ═══════════════════════════════════════════════════════════════════════════
def log(msg: str) -> None:
"""Simple stdout logger."""
print(f"[E2E] {msg}")
def fill_input(page, placeholder: str, value: str) -> None:
"""Locate an <input> by its placeholder and fill it."""
locator = page.locator(f'input[placeholder="{placeholder}"]')
locator.wait_for(state="visible", timeout=ACTION_TIMEOUT)
locator.fill(value)
log(f" Filled '{placeholder}''{value}'")
# ═══════════════════════════════════════════════════════════════════════════
# Main test flow
# ═══════════════════════════════════════════════════════════════════════════
def main() -> None:
with sync_playwright() as pw:
# ── 1. Launch browser (local or remote via BROWSERLESS_URL) ──
browserless_url = os.getenv("BROWSERLESS_URL")
if browserless_url:
# Connect to a remote Browserless / Chrome container
browser = pw.chromium.connect_over_cdp(browserless_url)
log(f"Connected to remote browser at {browserless_url}")
else:
# Local fallback
browser = pw.chromium.launch(
headless=os.getenv("HEADLESS", "True").lower() == "true",
slow_mo=SLOW_MO,
)
log(f"Browser launched locally (headless={os.getenv('HEADLESS', 'True')})")
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="hu-HU",
)
page = context.new_page()
log(f"Browser launched (headless={HEADLESS})")
# ── 2. Open landing page & login ─────────────────────────────
log("Navigating to landing page …")
page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
# Click the "Garázs Nyitása" button to open the LoginModal
login_trigger = page.locator("button:has-text('Garázs Nyitása')")
login_trigger.wait_for(state="visible", timeout=ACTION_TIMEOUT)
login_trigger.click()
log("Login modal opened")
# Fill email & password
page.fill("#login-email", TEST_EMAIL)
page.fill("#login-password", TEST_PASS)
log("Login credentials entered")
# Click the submit button (the first .btn-premium inside the visible form)
submit_btn = page.locator("form button[type='submit']").first
submit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
submit_btn.click()
log("Login submitted waiting for navigation …")
# Wait for redirect away from landing page (to /dashboard or /complete-kyc)
try:
page.wait_for_url(
lambda url: "/dashboard" in url or "/complete-kyc" in url,
timeout=15_000,
)
except PwTimeout:
# If still on landing, maybe login failed dump state
log(f"URL after login attempt: {page.url}")
page.screenshot(path="/tmp/e2e_login_fail.png")
raise AssertionError(
"Login failed did not redirect to dashboard. "
"Check credentials or UI state."
)
log(f"Login successful current URL: {page.url}")
# ── 3. Navigate to Profile page ──────────────────────────────
log("Navigating to /profile …")
page.goto(f"{BASE_URL}/profile", wait_until="networkidle", timeout=20_000)
# Wait for the profile page to finish loading (spinner disappears)
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
# Also wait until the loading spinner is gone
page.wait_for_function(
"() => !document.querySelector('.animate-spin')",
timeout=15_000,
)
log("Profile page loaded")
# ── 4. Enter edit mode ───────────────────────────────────────
edit_btn = page.locator("button:has-text('Szerkesztés')")
edit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
edit_btn.click()
log("Edit mode activated")
# ── 5. Fill in all address fields ────────────────────────────
log("Filling address form …")
fill_input(page, "Irányítószám", ADDRESS_DATA["address_zip"])
fill_input(page, "Város", ADDRESS_DATA["address_city"])
fill_input(page, "Utca", ADDRESS_DATA["address_street_name"])
fill_input(page, "Közterület jellege", ADDRESS_DATA["address_street_type"])
fill_input(page, "Házszám", ADDRESS_DATA["address_house_number"])
fill_input(page, "Lépcsőház", ADDRESS_DATA["address_stairwell"])
fill_input(page, "Emelet", ADDRESS_DATA["address_floor"])
fill_input(page, "Ajtó", ADDRESS_DATA["address_door"])
# ── 6. Save ──────────────────────────────────────────────────
save_btn = page.locator("button:has-text('Mentés')")
save_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
save_btn.click()
log("Save clicked waiting for confirmation …")
# Wait for the success message to appear
success_locator = page.locator("text=A profil adatok és okmányok sikeresen frissítve!")
try:
success_locator.wait_for(state="visible", timeout=10_000)
log("Save succeeded success message visible")
except PwTimeout:
# Check for error message
error_locator = page.locator(".text-red-400")
if error_locator.is_visible():
error_text = error_locator.text_content()
log(f"Save FAILED with error: {error_text}")
page.screenshot(path="/tmp/e2e_save_fail.png")
raise AssertionError(f"Save failed: {error_text}")
page.screenshot(path="/tmp/e2e_save_timeout.png")
raise AssertionError(
"Save timeout neither success nor error message appeared."
)
# ── 7. Reload and verify persistence ─────────────────────────
log("Reloading page to verify persistence …")
page.reload(wait_until="networkidle")
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
page.wait_for_function(
"() => !document.querySelector('.animate-spin')",
timeout=15_000,
)
log("Page reloaded")
# Read the display text in the read-only address section
# The read-only view shows: Lépcsőház: …, Emelet: …, Ajtó: …
page_body = page.locator("body").inner_text()
log("── Read-only address block ──")
# Use a regex to extract values
stairwell_match = re.search(r"Lépcsőház:\s*(\S+)", page_body)
floor_match = re.search(r"Emelet:\s*(\S+)", page_body)
door_match = re.search(r"Ajtó:\s*(\S+)", page_body)
stairwell_val = stairwell_match.group(1) if stairwell_match else ""
floor_val = floor_match.group(1) if floor_match else ""
door_val = door_match.group(1) if door_match else ""
log(f" Stairwell (Lépcsőház): '{stairwell_val}'")
log(f" Floor (Emelet): '{floor_val}'")
log(f" Door (Ajtó): '{door_val}'")
# ── 8. Assertion: all three values must be present ───────────
errors = []
if not stairwell_val or stairwell_val in ("", "null", "None"):
errors.append(f"stairwell (expected '{ADDRESS_DATA['address_stairwell']}', got '{stairwell_val}')")
if not floor_val or floor_val in ("", "null", "None"):
errors.append(f"floor (expected '{ADDRESS_DATA['address_floor']}', got '{floor_val}')")
if not door_val or door_val in ("", "null", "None"):
errors.append(f"door (expected '{ADDRESS_DATA['address_door']}', got '{door_val}')")
if errors:
page.screenshot(path="/tmp/e2e_persistence_fail.png")
raise AssertionError(
"Hibás backend mentés! Az emelet/ajtó adatok elvesztek az oldal "
f"frissítése után.\n Missing/empty fields: {', '.join(errors)}"
)
log("Sikeres E2E teszt: Az adatok megmaradtak a backend frissítés után is!")
log("ALL CHECKS PASSED.\n")
browser.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,31 @@
import asyncio
from sqlalchemy import text
from app.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as session:
print("--- TASK 1: BRANCHES (GARAGES) ---")
try:
result = await session.execute(text("SELECT id, organization_id, name FROM fleet.branches;"))
branches = result.fetchall()
print("| id | organization_id | name |")
print("|---|---|---|")
for b in branches:
print(f"| {b.id} | {b.organization_id} | {b.name} |")
except Exception as e:
print(f"Error querying fleet.branches: {e}")
print("\n--- TASK 2: ASSETS (VEHICLES) ---")
try:
# We will query all assets and print the relevant columns.
result = await session.execute(text("SELECT id, license_plate, owner_person_id, owner_org_id, branch_id FROM vehicle.assets LIMIT 50;"))
assets = result.fetchall()
print("| id | license_plate | owner_person_id | owner_org_id | branch_id |")
print("|---|---|---|---|---|")
for a in assets:
print(f"| {a.id} | {a.license_plate} | {a.owner_person_id} | {a.owner_org_id} | {a.branch_id} |")
except Exception as e:
print(f"Error querying vehicle.assets: {e}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,137 @@
import asyncio
import httpx
from sqlalchemy import select, delete
from app.db.session import AsyncSessionLocal
from app.models.identity import User, Person
from app.models.marketplace.organization import Organization, Branch, OrganizationMember
from app.models.vehicle.asset import Asset
from app.models.vehicle.vehicle_definitions import VehicleModelDefinition
from app.core.security import create_tokens
import uuid
async def setup_test_data():
async with AsyncSessionLocal() as db:
# 1. Clean previous test data
await db.execute(delete(Asset).where(Asset.license_plate.in_(["TEST-123", "DRAFT-456", "OTHER-123"])))
await db.execute(delete(VehicleModelDefinition).where(VehicleModelDefinition.make == "Ford", VehicleModelDefinition.marketing_name == "Focus"))
await db.execute(delete(Branch).where(Branch.name == "Test Main Branch"))
await db.execute(delete(OrganizationMember))
await db.execute(delete(Organization).where(Organization.name == "Test Org"))
test_users = await db.execute(select(User).where(User.email.in_(["test1@test.com", "test2@test.com"])))
for u in test_users.scalars().all():
await db.execute(delete(User).where(User.id == u.id))
await db.commit()
# 2. Create Person
person1 = Person(first_name="Test1", last_name="User1")
person2 = Person(first_name="Test2", last_name="User2")
db.add_all([person1, person2])
await db.flush()
# 3. Create Users
user1 = User(email="test1@test.com", hashed_password="pw", is_active=True, person_id=person1.id, subscription_plan="personal", is_vip=False, preferred_language="en", region_code="HU", preferred_currency="EUR", scope_level="personal", is_vip=False, preferred_language="en", region_code="HU", preferred_currency="EUR")
user2 = User(email="test2@test.com", hashed_password="pw", is_active=True, person_id=person2.id, subscription_plan="personal", is_vip=False, preferred_language="en", region_code="HU", preferred_currency="EUR", scope_level="personal", is_vip=False, preferred_language="en", region_code="HU", preferred_currency="EUR")
db.add_all([user1, user2])
await db.flush()
# 4. Create Organization & Branch
org = Organization(name="Test Org", tax_number="123456")
db.add(org)
await db.flush()
member1 = OrganizationMember(organization_id=org.id, user_id=user1.id, role="owner")
member2 = OrganizationMember(organization_id=org.id, user_id=user2.id, role="member")
db.add_all([member1, member2])
branch = Branch(organization_id=org.id, name="Test Main Branch", is_main=True)
db.add(branch)
# 5. Create VehicleModelDefinition
model_def = VehicleModelDefinition(
make="Ford",
marketing_name="Focus",
normalized_name="ford_focus",
year_from=2015,
year_to=2020,
power_kw=92,
data_status="verified"
)
db.add(model_def)
await db.commit()
# Generate tokens
token1, _ = create_tokens(data={"sub": str(user1.id)})
token2, _ = create_tokens(data={"sub": str(user2.id)})
return token1, token2, org.id, branch.id, model_def.id
async def run_tests():
print("--- SETUP ---")
token1, token2, org_id, branch_id, model_id = await setup_test_data()
print("Test data created successfully.")
headers1 = {"Authorization": f"Bearer {token1}"}
headers2 = {"Authorization": f"Bearer {token2}"}
async with httpx.AsyncClient(base_url="http://127.0.0.1:8000") as client:
print("\n--- TEST 1: CATALOG MATCH & GARAGE TEST ---")
payload1 = {
"vin": "TESTVIN1234567890",
"license_plate": "TEST-123",
"brand": "Ford",
"model": "Focus",
"year_of_manufacture": 2018,
"organization_id": org_id,
"vehicle_class": "car",
"fuel_type": "petrol"
}
resp1 = await client.post("/api/v1/vehicles", json=payload1, headers=headers1)
print(f"Status: {resp1.status_code}")
try:
data1 = resp1.json()
print(f"Response: {data1}")
if resp1.status_code == 201:
assert data1["catalog_id"] == model_id, f"Matcher failed: expected {model_id}, got {data1.get('catalog_id')}"
assert data1["branch_id"] == str(branch_id), f"Branch failed: expected {branch_id}, got {data1.get('branch_id')}"
print("-> SUCCESS: Matcher assigned catalog and Main Branch assigned.")
except Exception as e:
print(f"-> FAILED: {e}")
print("\n--- TEST 2: DRAFT / INCOMPLETE TEST ---")
payload2 = {
"license_plate": "DRAFT-456",
"brand": "Ismeretlen",
"organization_id": org_id
}
resp2 = await client.post("/api/v1/vehicles", json=payload2, headers=headers1)
print(f"Status: {resp2.status_code}")
try:
data2 = resp2.json()
print(f"Response: {data2}")
if resp2.status_code == 201:
assert data2["status"] == "draft", f"Status failed: expected 'draft', got {data2.get('status')}"
assert data2["branch_id"] == str(branch_id), f"Branch failed: expected {branch_id}, got {data2.get('branch_id')}"
print("-> SUCCESS: Draft created and Main Branch assigned.")
except Exception as e:
print(f"-> FAILED: {e}")
print("\n--- TEST 3: OWNERSHIP TRANSFER TEST ---")
payload3 = {
"vin": "TESTVIN1234567890",
"license_plate": "OTHER-123",
"brand": "Ford",
"model": "Focus",
"organization_id": org_id
}
resp3 = await client.post("/api/v1/vehicles", json=payload3, headers=headers2)
print(f"Status: {resp3.status_code}")
try:
data3 = resp3.json()
print(f"Response: {data3}")
if resp3.status_code == 202:
print("-> SUCCESS: VIN collision detected (transfer_pending).")
except Exception as e:
print(f"-> FAILED: {e}")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,134 @@
import asyncio
import httpx
from sqlalchemy import text
from app.db.session import AsyncSessionLocal
from app.services.auth_service import AuthService
async def get_verification_token(email: str):
async with AsyncSessionLocal() as session:
result = await session.execute(
text("SELECT id FROM identity.users WHERE email = :email"),
{"email": email}
)
user_id = result.scalar()
if not user_id:
return None
result = await session.execute(
text("SELECT token FROM identity.verification_tokens WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 1"),
{"user_id": user_id}
)
return result.scalar()
async def get_user_state(email: str):
async with AsyncSessionLocal() as session:
result = await session.execute(
text("SELECT id, email, is_active FROM identity.users WHERE email = :email"),
{"email": email}
)
row = result.fetchone()
if row:
return {"id": row[0], "email": row[1], "is_active": row[2]}
result = await session.execute(
text("SELECT id, email, is_active FROM identity.users WHERE email LIKE 'deleted_%' ORDER BY created_at DESC LIMIT 1")
)
row = result.fetchone()
if row:
return {"id": row[0], "email": row[1], "is_active": row[2]}
return None
async def run_tests():
base_url = "http://localhost:8000/api/v1"
email = "test_architect@example.com"
password = "TestPassword123!"
async with httpx.AsyncClient() as client:
# Cleanup
async with AsyncSessionLocal() as session:
await session.execute(text("DELETE FROM audit.audit_logs WHERE user_id IN (SELECT id FROM identity.users WHERE email = :email OR email LIKE 'deleted_%')"), {"email": email})
await session.execute(text("DELETE FROM identity.verification_tokens WHERE user_id IN (SELECT id FROM identity.users WHERE email = :email OR email LIKE 'deleted_%')"), {"email": email})
await session.execute(text("DELETE FROM identity.persons WHERE user_id IN (SELECT id FROM identity.users WHERE email = :email OR email LIKE 'deleted_%')"), {"email": email})
await session.execute(text("DELETE FROM identity.users WHERE email = :email OR email LIKE 'deleted_%'"), {"email": email})
await session.commit()
print("--- 1. LITE REGISTRATION TEST ---")
reg_payload = {
"email": email,
"password": password,
"first_name": "Test",
"last_name": "Architect"
}
resp = await client.post(f"{base_url}/auth/register", json=reg_payload)
print(f"Register status: {resp.status_code}")
print(f"Register response: {resp.text}")
assert resp.status_code == 201, "Registration failed"
user_state = await get_user_state(email)
assert user_state is not None, "User not found in DB"
assert user_state["is_active"] == False, "User should be inactive initially"
print("Registration Test PASSED.\n")
print("--- 2. EMAIL VERIFICATION TEST ---")
token = await get_verification_token(email)
assert token is not None, "Verification token not found"
print(f"Got token: {token}")
verify_resp = await client.post(f"{base_url}/auth/verify-email", json={"token": str(token)})
print(f"Verify status: {verify_resp.status_code}")
print(f"Verify response: {verify_resp.text}")
assert verify_resp.status_code == 200, "Verification failed"
user_state = await get_user_state(email)
assert user_state["is_active"] == True, "User should be active after verification"
print("Email Verification Test PASSED.\n")
print("--- 3. REMEMBER ME / COOKIE TEST ---")
login_data = {
"username": email,
"password": password,
"grant_type": "password",
"remember_me": "true"
}
# In fastapi/OAuth2 password flow, we can use Form data for username/password
login_resp = await client.post(f"{base_url}/auth/login", data=login_data)
print(f"Login status: {login_resp.status_code}")
print(f"Login response: {login_resp.text}")
assert login_resp.status_code == 200, "Login failed"
set_cookie_headers = login_resp.headers.get_list('set-cookie')
print(f"Set-Cookie headers: {set_cookie_headers}")
found_refresh = False
for h in set_cookie_headers:
if "refresh_token=" in h:
found_refresh = True
assert "HttpOnly" in h or "httponly" in h.lower(), "refresh_token must be HttpOnly"
# assert "Secure" in h or "secure" in h.lower(), "refresh_token must be Secure"
assert "samesite=lax" in h.lower(), "refresh_token must have SameSite=lax"
assert "Max-Age=" in h or "max-age=" in h.lower(), "refresh_token must have Max-Age"
assert "2592000" in h, f"Max-Age must be 30 days (2592000), got: {h}"
assert found_refresh, "refresh_token Set-Cookie header missing"
print("Remember Me / Cookie Test PASSED.\n")
print("--- 4. SOFT DELETE / ANONYMIZATION TEST ---")
access_token = login_resp.json().get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
async with AsyncSessionLocal() as session:
await AuthService.soft_delete_user(session, user_state["id"], "Test deletion", user_state["id"])
delete_resp_status_code = 200
print(f"Delete status: {delete_resp_status_code}")
print("Delete response: success")
assert delete_resp_status_code == 200
user_state = await get_user_state(email)
assert user_state["email"].startswith("deleted_"), f"Email not anonymized: {user_state['email']}"
assert user_state["is_active"] == False, "User not deactivated"
print("Soft Delete Test PASSED.\n")
print("ALL TESTS COMPLETED SUCCESSFULLY!")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,31 @@
import asyncio
from sqlalchemy import text
from app.db.session import AsyncSessionLocal
async def setup_prerequisites():
print("--- Setting up Prerequisites ---")
async with AsyncSessionLocal() as session:
params = [
("auth_remember_me_days", '{"value": 30}'),
("auth_refresh_default_days", '{"value": 1}'),
("auth_password_strict", '{"value": true}')
]
for key, value in params:
# check if exists
res = await session.execute(text("SELECT key FROM system.system_parameters WHERE key = :key"), {"key": key})
if res.scalar():
await session.execute(
text("UPDATE system.system_parameters SET value = CAST(:value AS jsonb) WHERE key = :key"),
{"key": key, "value": value}
)
else:
await session.execute(
text("INSERT INTO system.system_parameters (key, value, is_active) VALUES (:key, CAST(:value AS jsonb), true)"),
{"key": key, "value": value}
)
await session.commit()
print("Prerequisites setup complete.\n")
if __name__ == "__main__":
asyncio.run(setup_prerequisites())

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""
Test script to verify catalog filtering by vehicle_class.
Run inside sf_api container: docker compose exec sf_api python /opt/docker/dev/service_finder/test_catalog_filter.py
"""
import asyncio
import sys
import os
sys.path.insert(0, '/app/backend')
from app.db.session import async_sessionmaker
from app.services.asset_service import AssetService
async def test_catalog_filter():
async with async_sessionmaker() as session:
async with session.begin():
# Get first make
makes = await AssetService.get_makes(session)
if not makes:
print("No makes found in database")
return
test_make = makes[0]
print(f"Testing with make: {test_make}")
# Get models without filter
models_all = await AssetService.get_models(session, test_make)
print(f"All models ({len(models_all)}): {models_all[:3]}...")
# Get models with vehicle_class filter (passenger_car)
models_filtered = await AssetService.get_models(session, test_make, 'passenger_car')
print(f"Filtered by 'passenger_car' ({len(models_filtered)}): {models_filtered[:3]}...")
# Get models with vehicle_class filter (motorcycle)
models_motorcycle = await AssetService.get_models(session, test_make, 'motorcycle')
print(f"Filtered by 'motorcycle' ({len(models_motorcycle)}): {models_motorcycle[:3]}...")
# Compare counts
if models_filtered:
print("✓ Filtering by vehicle_class works")
if len(models_filtered) <= len(models_all):
print("✓ Filtered count is less or equal (good)")
else:
print("⚠ Filtered count is greater (unexpected)")
else:
print("⚠ No models found with filter (maybe no passenger_car vehicles for this make)")
if __name__ == "__main__":
asyncio.run(test_catalog_filter())

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
Comprehensive test for the organization switching flow with token refresh.
Tests the complete lifecycle:
1. Login as tester_pro
2. Get current user info and organizations
3. Switch between organizations (Private, Alpha, Beta)
4. Verify token refresh works
5. Verify vehicles are filtered by scope
"""
import asyncio
import aiohttp
import json
import sys
from typing import Dict, Any, List
API_BASE = "http://localhost:8000" # sf_api container
async def make_request(session: aiohttp.ClientSession, method: str, endpoint: str,
token: str = None, data: Dict = None) -> Dict[str, Any]:
"""Helper function to make HTTP requests"""
url = f"{API_BASE}{endpoint}"
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
try:
async with session.request(method, url, json=data, headers=headers) as response:
response_text = await response.text()
try:
response_data = json.loads(response_text) if response_text else {}
except json.JSONDecodeError:
response_data = {"raw": response_text}
if not response.ok:
print(f"❌ Request failed: {method} {endpoint} - {response.status}")
print(f" Response: {response_data}")
return {"error": True, "status": response.status, "data": response_data}
return {"error": False, "status": response.status, "data": response_data}
except Exception as e:
print(f"❌ Request exception: {method} {endpoint} - {e}")
return {"error": True, "exception": str(e)}
async def login(session: aiohttp.ClientSession, email: str, password: str) -> str:
"""Login and return access token"""
print(f"\n🔐 Logging in as {email}...")
form_data = aiohttp.FormData()
form_data.add_field('username', email)
form_data.add_field('password', password)
async with session.post(f"{API_BASE}/auth/login", data=form_data) as response:
if response.status == 200:
data = await response.json()
token = data.get('access_token')
if token:
print(f"✅ Login successful, token: {token[:30]}...")
return token
else:
print(f"❌ No token in response: {data}")
return None
else:
text = await response.text()
print(f"❌ Login failed: {response.status} - {text}")
return None
async def get_user_info(session: aiohttp.ClientSession, token: str) -> Dict[str, Any]:
"""Get current user information"""
print("\n👤 Getting user info...")
result = await make_request(session, "GET", "/users/me", token)
if not result["error"]:
user_data = result["data"]
print(f"✅ User info retrieved:")
print(f" ID: {user_data.get('id')}")
print(f" Email: {user_data.get('email')}")
print(f" Role: {user_data.get('role')}")
print(f" Scope ID: {user_data.get('scope_id')}")
print(f" Active Org ID: {user_data.get('active_organization_id')}")
print(f" Person ID: {user_data.get('person_id')}")
return user_data
return None
async def get_user_organizations(session: aiohttp.ClientSession, token: str) -> List[Dict[str, Any]]:
"""Get organizations for the current user"""
print("\n🏢 Getting user organizations...")
result = await make_request(session, "GET", "/organizations/me", token)
if not result["error"]:
orgs = result["data"]
print(f"✅ Found {len(orgs)} organizations:")
for org in orgs:
print(f" - ID: {org.get('id')}, Name: {org.get('name')}, Type: {org.get('org_type')}")
return orgs
return []
async def switch_organization(session: aiohttp.ClientSession, token: str, org_id: int) -> Dict[str, Any]:
"""Switch to a different organization and return new token"""
print(f"\n🔄 Switching to organization ID {org_id}...")
result = await make_request(session, "PATCH", "/users/me/active-organization", token,
{"organization_id": org_id})
if not result["error"]:
response_data = result["data"]
print(f"✅ Organization switch response:")
print(f" Has user data: {'user' in response_data}")
print(f" Has access_token: {'access_token' in response_data}")
print(f" Token type: {response_data.get('token_type', 'N/A')}")
if 'access_token' in response_data:
new_token = response_data['access_token']
print(f" New token: {new_token[:30]}...")
print(f" Token changed: {new_token != token}")
return {"success": True, "new_token": new_token, "response": response_data}
else:
print(f"⚠️ No new token in response (old format?)")
return {"success": False, "response": response_data}
else:
print(f"❌ Organization switch failed")
return {"success": False, "error": result}
async def get_user_vehicles(session: aiohttp.ClientSession, token: str) -> List[Dict[str, Any]]:
"""Get vehicles for the current user (filtered by scope)"""
print("\n🚗 Getting user vehicles (scope-filtered)...")
result = await make_request(session, "GET", "/users/me/assets", token)
if not result["error"]:
vehicles = result["data"]
print(f"✅ Found {len(vehicles)} vehicles in current scope:")
for vehicle in vehicles:
print(f" - ID: {vehicle.get('id')}, VRM: {vehicle.get('vrm')}, "
f"Make: {vehicle.get('make')}, Model: {vehicle.get('model')}")
return vehicles
return []
async def decode_token(token: str) -> Dict[str, Any]:
"""Decode JWT token to see payload"""
try:
import base64
import json as json_module
parts = token.split('.')
if len(parts) == 3:
payload = parts[1]
# Add padding if needed
padding = 4 - len(payload) % 4
if padding != 4:
payload += '=' * padding
decoded = base64.b64decode(payload)
return json_module.loads(decoded)
except Exception as e:
print(f"❌ Could not decode token: {e}")
return {}
async def main():
"""Main test flow"""
print("=" * 60)
print("🧪 COMPREHENSIVE ORGANIZATION SWITCHING FLOW TEST")
print("=" * 60)
# Test credentials
email = "tester_pro@profibot.hu"
password = "Password123!" # From reset script
async with aiohttp.ClientSession() as session:
# 1. Login
token = await login(session, email, password)
if not token:
print("❌ Cannot proceed without login")
return
# 2. Get initial user info
user_info = await get_user_info(session, token)
if not user_info:
print("❌ Cannot get user info")
return
# 3. Get organizations
orgs = await get_user_organizations(session, token)
if not orgs:
print("❌ No organizations found")
return
# Map organizations by type for easier switching
org_map = {}
for org in orgs:
org_type = org.get('org_type', 'UNKNOWN')
org_map[org_type] = org.get('id')
print(f" {org_type}: ID {org.get('id')} - {org.get('name')}")
# 4. Test switching to each organization
test_results = {}
for org_type, org_id in org_map.items():
print(f"\n{'='*40}")
print(f"🧪 Testing switch to {org_type} (ID: {org_id})")
print(f"{'='*40}")
# Switch organization
switch_result = await switch_organization(session, token, org_id)
if switch_result["success"] and "new_token" in switch_result:
new_token = switch_result["new_token"]
# Decode new token to verify scope_id
decoded = await decode_token(new_token)
print(f"🔍 Decoded new token payload:")
print(f" Scope ID: {decoded.get('scope_id')}")
print(f" Scope Level: {decoded.get('scope_level')}")
print(f" Role: {decoded.get('role')}")
# Update token for next requests
token = new_token
# Get user info with new token
user_info = await get_user_info(session, token)
# Get vehicles in new scope
vehicles = await get_user_vehicles(session, token)
test_results[org_type] = {
"success": True,
"scope_id": decoded.get('scope_id'),
"vehicles_count": len(vehicles),
"vehicles": [v.get('vrm') for v in vehicles]
}
else:
test_results[org_type] = {
"success": False,
"error": switch_result.get("error", "Unknown error")
}
# 5. Summary
print(f"\n{'='*60}")
print("📊 TEST SUMMARY")
print(f"{'='*60}")
all_passed = True
for org_type, result in test_results.items():
if result["success"]:
print(f"{org_type}: PASSED")
print(f" Scope ID: {result['scope_id']}")
print(f" Vehicles in scope: {result['vehicles_count']}")
if result['vehicles_count'] > 0:
print(f" Vehicle VRMs: {', '.join(result['vehicles'])}")
else:
print(f"{org_type}: FAILED")
print(f" Error: {result.get('error', 'Unknown')}")
all_passed = False
# 6. Final verification
print(f"\n{'='*40}")
print("🔍 FINAL VERIFICATION")
print(f"{'='*40}")
if all_passed:
print("🎉 ALL TESTS PASSED! The organization switching flow with token refresh is working correctly.")
# Verify database state
print("\n📋 DATABASE STATE VERIFICATION:")
print("1. tester_pro has person_id=29, user_id=28")
print("2. Private Organization (ID 21) has owner_id=29")
print("3. Alpha Organization (ID 26) has owner_id=29")
print("4. Beta Organization (ID 27) has owner_id=29")
print("5. Each organization has 1 branch")
print("6. Vehicles distributed: AAA111 to Private, AAA111 to Alpha, AAA222 to Beta")
print("7. Asset assignments created with proper UUIDs")
return 0
else:
print("❌ SOME TESTS FAILED. Check the errors above.")
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

121
tests/active/test_email.py Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Test Email Script — Service Finder
====================================
Tests the EmailManager by sending a test email using the configured provider.
Reads all settings from .env automatically via the app's Settings class.
Usage:
# Test with configured provider (Brevo API / SMTP):
docker compose exec sf_api python3 /app/test_email.py
# Test with local Mailpit (no real email sent, no TLS needed):
docker compose exec sf_api /bin/sh -c " \
SMTP_HOST=sf_mailpit SMTP_PORT=1025 SMTP_USER= SMTP_PASSWORD= \
EMAIL_PROVIDER=smtp TEST_RECIPIENT=kincses@gmail.com \
python3 /app/test_email.py"
# Test with Brevo API directly:
docker compose exec sf_api /bin/sh -c " \
EMAIL_PROVIDER=brevo_api TEST_RECIPIENT=kincses@gmail.com \
python3 /app/test_email.py"
Environment check:
- EMAIL_PROVIDER=smtp → uses SMTP
- EMAIL_PROVIDER=brevo_smtp → uses Brevo SMTP relay
- EMAIL_PROVIDER=brevo_api → uses Brevo REST API
"""
import os
import sys
import asyncio
import logging
# Ensure the backend package is importable
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# ── Load .env but do NOT override already-set env vars ──
from dotenv import load_dotenv
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env')
load_dotenv(env_path, override=False) # override=False = respect existing env vars
# ── Logging setup ──
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger("TestEmail")
async def main():
"""Send a test email and report the result."""
print("=" * 70)
print(" SERVICE FINDER — EMAIL TEST SCRIPT")
print("=" * 70)
# ── 1. Show current email configuration ──
provider = os.getenv("EMAIL_PROVIDER", "smtp")
print(f"\n📧 Current EMAIL_PROVIDER: {provider}")
print(f" SMTP_HOST: {os.getenv('SMTP_HOST', '(not set)')}")
print(f" SMTP_PORT: {os.getenv('SMTP_PORT', '(not set)')}")
print(f" SMTP_USER: {os.getenv('SMTP_USER', '(not set)')}")
print(f" MAIL_FROM: {os.getenv('MAIL_FROM', '(not set)')}")
print(f" MAIL_FROM_NAME: {os.getenv('MAIL_FROM_NAME', '(not set)')}")
# Brevo-specific checks
brevo_api_key = os.getenv("BREVO_API_KEY")
brevo_smtp_user = os.getenv("BREVO_SMTP_USER")
brevo_smtp_pass = os.getenv("BREVO_SMTP_PASSWORD")
if provider == "brevo_api":
print(f" BREVO_API_KEY: {'✅ Set' if brevo_api_key else '❌ NOT SET'}")
elif provider == "brevo_smtp":
print(f" BREVO_SMTP_USER: {'✅ Set' if brevo_smtp_user else '❌ NOT SET'}")
print(f" BREVO_SMTP_PASSWORD: {'✅ Set' if brevo_smtp_pass else '❌ NOT SET'}")
# ── 2. Recipient ──
recipient = os.getenv("TEST_RECIPIENT") or os.getenv("INITIAL_ADMIN_EMAIL") or "kincses@valami.hu"
print(f"\n📨 Test recipient: {recipient}")
# ── 3. Import and send ──
print("\n⏳ Sending test email...\n")
try:
from app.services.email_manager import EmailManager
result = await EmailManager.send_email(
recipient=recipient,
template_key="test",
variables={
"first_name": "Teszt",
"link": "https://dev.servicefinder.hu/dashboard",
},
lang="hu",
)
print(f"\n{'=' * 70}")
print(f" 📬 EMAIL RESULT")
print(f"{'=' * 70}")
print(f" Status: {result.get('status', 'unknown')}")
print(f" Provider: {result.get('provider', 'unknown')}")
if result.get('message_id'):
print(f" MessageID: {result['message_id']}")
if result.get('message'):
print(f" Message: {result['message']}")
print(f"{'=' * 70}\n")
if result.get("status") == "success":
print("✅ TEST PASSED — Email sent successfully!")
return 0
else:
print(f"❌ TEST FAILED — {result.get('message', 'Unknown error')}")
return 1
except Exception as e:
print(f"\n❌ CRITICAL ERROR: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Test script to verify i18n implementation.
Tests the context management, middleware logic, and translation service integration.
"""
import asyncio
import sys
import os
# Add the backend directory to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
async def test_context_management():
"""Test context.py functionality"""
print("=== Testing Context Management ===")
from app.core.context import get_current_locale, set_current_locale
# Test default locale
default_locale = get_current_locale()
print(f"Default locale: {default_locale} (expected: 'hu')")
assert default_locale == "hu", f"Expected 'hu', got {default_locale}"
# Test setting and getting locale
set_current_locale("en")
current_locale = get_current_locale()
print(f"After set to 'en': {current_locale}")
assert current_locale == "en", f"Expected 'en', got {current_locale}"
# Test reset to default
set_current_locale("hu")
current_locale = get_current_locale()
print(f"After reset to 'hu': {current_locale}")
assert current_locale == "hu", f"Expected 'hu', got {current_locale}"
print("✅ Context management test passed\n")
return True
def test_translation_helper():
"""Test translation_helper.py functionality"""
print("=== Testing Translation Helper ===")
from app.core.translation_helper import t, get_text, translate
# Test that all aliases point to the same function
assert t is get_text, "t and get_text should be the same function"
assert t is translate, "t and translate should be the same function"
# Test basic function signature
import inspect
sig = inspect.signature(t)
params = list(sig.parameters.keys())
print(f"Function parameters: {params}")
assert 'key' in params, "Missing 'key' parameter"
assert 'variables' in params, "Missing 'variables' parameter"
assert 'lang' in params, "Missing 'lang' parameter"
print("✅ Translation helper test passed\n")
return True
def test_middleware_logic():
"""Test i18n_middleware.py logic (without FastAPI)"""
print("=== Testing Middleware Logic ===")
from app.core.i18n_middleware import I18nMiddleware
# Create a dummy ASGI app for middleware instantiation
class DummyApp:
async def __call__(self, scope, receive, send):
pass
middleware = I18nMiddleware(DummyApp())
# Test locale validation
test_cases = [
("en", True), # Valid
("hu", True), # Valid
("de", True), # Valid
("en-US", True), # Valid with region
("", False), # Empty string
("123", False), # Numbers
("a" * 6, False), # Too long
("en_US", True), # Underscore (should be valid after cleaning)
]
for locale, expected in test_cases:
result = middleware._is_valid_locale(locale)
print(f" {locale}: {result} (expected: {expected})")
# Note: Some edge cases might differ, so we'll just log
# Test Accept-Language parsing
test_headers = [
("en-US,en;q=0.9", "en"),
("hu,en;q=0.8", "hu"),
("de-DE,de;q=0.7,en;q=0.5", "de"),
("fr,en;q=0.9", "fr"),
("", None), # Empty header
]
for header, expected in test_headers:
result = middleware._parse_accept_language(header)
print(f" '{header}' -> {result} (expected: {expected})")
# Just log, don't assert due to potential edge cases
print("✅ Middleware logic test passed\n")
return True
def test_imports():
"""Test that all modified files can be imported"""
print("=== Testing Imports ===")
modules_to_test = [
"app.core.context",
"app.core.i18n_middleware",
"app.core.translation_helper",
"app.services.translation_service",
"app.api.deps",
"app.api.v1.endpoints.auth",
]
for module_name in modules_to_test:
try:
__import__(module_name)
print(f"{module_name}")
except ImportError as e:
print(f"{module_name}: {e}")
# Don't fail the test, just log
print("✅ Import test completed\n")
return True
async def main():
"""Run all tests"""
print("🧪 Testing Service Finder i18n Implementation\n")
tests = [
test_context_management,
test_translation_helper,
test_middleware_logic,
test_imports,
]
all_passed = True
for test in tests:
try:
if asyncio.iscoroutinefunction(test):
result = await test()
else:
result = test()
all_passed = all_passed and result
except Exception as e:
print(f"❌ Test failed with exception: {e}")
import traceback
traceback.print_exc()
all_passed = False
if all_passed:
print("🎉 All tests passed! The i18n implementation is ready.")
else:
print("⚠️ Some tests failed. Please review the implementation.")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
Integration test for the Smart Vehicle Registration fixes.
Tests: login, catalog filtering, vehicle registration.
"""
import asyncio
import sys
import os
sys.path.insert(0, '/app')
from fastapi.testclient import TestClient
from app.api.v1.api import api_router
from fastapi import FastAPI
from app.core.config import settings
import json
app = FastAPI()
app.include_router(api_router)
client = TestClient(app)
def test_login():
"""Login with tester_pro@profibot.hu and Password123!"""
print("1. Testing login...")
response = client.post("/auth/login", json={
"email": "tester_pro@profibot.hu",
"password": "Password123!"
})
if response.status_code != 200:
print(f" ❌ Login failed: {response.status_code} {response.text}")
return None
token = response.json().get("access_token")
print(f" ✅ Login successful, token: {token[:20]}...")
return token
def test_catalog_makes(token):
"""Test catalog makes endpoint with auth."""
print("2. Testing catalog makes...")
response = client.get("/catalog/makes", headers={"Authorization": f"Bearer {token}"})
if response.status_code != 200:
print(f" ❌ Makes failed: {response.status_code} {response.text}")
return None
makes = response.json()
print(f" ✅ Makes retrieved: {len(makes)} items")
return makes
def test_catalog_models_filter(token, make, vehicle_class):
"""Test catalog models endpoint with vehicle_class filter."""
print(f"3. Testing catalog models with make={make}, vehicle_class={vehicle_class}...")
params = {"make": make}
if vehicle_class:
params["vehicle_class"] = vehicle_class
response = client.get("/catalog/models", headers={"Authorization": f"Bearer {token}"}, params=params)
if response.status_code != 200:
print(f" ❌ Models failed: {response.status_code} {response.text}")
return None
models = response.json()
print(f" ✅ Models retrieved: {len(models)} items")
return models
def test_vehicle_registration(token, org_id=None):
"""Test vehicle registration endpoint with a minimal payload."""
print("4. Testing vehicle registration...")
payload = {
"license_plate": "TEST-123",
"brand": "Toyota",
"model": "Corolla",
"vehicle_class": "passenger_car",
"fuel_type": "petrol",
"current_mileage": 50000,
"status": "draft",
"organization_id": org_id,
"owner_org_id": org_id,
"operator_org_id": org_id,
}
response = client.post("/assets/vehicles",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=payload)
if response.status_code not in (200, 201):
print(f" ❌ Registration failed: {response.status_code} {response.text}")
return None
result = response.json()
print(f" ✅ Vehicle registered: ID {result.get('id')}")
return result
def main():
print("=== Integration Test for Smart Vehicle Registration Fixes ===")
token = test_login()
if not token:
print("❌ Test aborted due to login failure.")
return
makes = test_catalog_makes(token)
if not makes:
print("❌ Test aborted due to catalog failure.")
return
# Test filtering
test_make = makes[0] if makes else "Toyota"
models_all = test_catalog_models_filter(token, test_make, None)
models_car = test_catalog_models_filter(token, test_make, "passenger_car")
models_motorcycle = test_catalog_models_filter(token, test_make, "motorcycle")
# Compare counts
if models_all and models_car:
if len(models_car) <= len(models_all):
print(" ✅ Filtering works (car count <= all count)")
else:
print(" ⚠ Filtering anomaly (car count > all count)")
# Test registration (requires organization ID, but we can try without)
# First get user's organizations
print("5. Fetching user organizations...")
response = client.get("/organizations/my", headers={"Authorization": f"Bearer {token}"})
if response.status_code == 200:
orgs = response.json()
if orgs:
org_id = orgs[0].get("organization_id")
print(f" Using organization ID: {org_id}")
test_vehicle_registration(token, org_id)
else:
print(" No organizations, testing without org...")
test_vehicle_registration(token, None)
else:
print(f" Could not fetch organizations: {response.status_code}")
test_vehicle_registration(token, None)
print("=== Integration Test Completed ===")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,174 @@
import asyncio
import logging
import sys
from datetime import datetime, date
import uuid
# Hozzáadjuk az app modul eléréséhez
sys.path.append("/app/backend")
from app.db.session import AsyncSessionLocal
from app.models.identity import User, Person, VerificationToken
from app.models.marketplace import Organization, OrganizationMember
from app.services.auth_service import AuthService
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from sqlalchemy import select, delete, text
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def run_tests():
async with AsyncSessionLocal() as db:
logger.info("--- TESZTKÖRNYEZET ELŐKÉSZÍTÉSE ---")
suffix = uuid.uuid4().hex[:6]
email_owner = f"owner_{suffix}@test.com"
email_existing = f"existing_{suffix}@test.com"
email_newbie = f"newbie_{suffix}@test.com"
email_shadow = f"shadow_{suffix}@test.com"
shadow_name = f"ShadowBéla_{suffix}"
# 1. Alap adatok: Tulajdonos regisztrál és KYC
owner_in = UserLiteRegister(
email=email_owner,
password="Password123!",
first_name="Tulajdonos",
last_name="Teszt",
region_code="HU",
lang="hu"
)
owner_user = await AuthService.register_lite(db, owner_in)
owner_user.is_active = True
person = (await db.execute(select(Person).where(Person.id == owner_user.person_id))).scalar_one()
person.is_active = True
await db.commit()
kyc_owner = UserKYCComplete(
phone_number="+36301234567",
birth_place="Budapest",
birth_date=date(1980, 1, 1),
mothers_last_name="Anyja",
mothers_first_name="Neve",
address_zip="1111",
address_city="Budapest",
address_street_name="Teszt",
address_street_type="utca",
address_house_number="1",
identity_docs={},
ice_contact={"name": "Teszt", "phone": "123", "relationship": "barat"}
)
owner_user = await AuthService.complete_kyc(db, owner_user.id, kyc_owner)
org_id = int(owner_user.scope_id)
logger.info(f"Tulajdonos regisztrálva. Org ID: {org_id}")
existing_in = UserLiteRegister(
email=email_existing, password="Password123!", first_name="Létező", last_name="Teszt", region_code="HU", lang="hu"
)
existing_user = await AuthService.register_lite(db, existing_in)
await db.commit()
from app.api.v1.endpoints.organizations import invite_to_organization, accept_invitation, OrgInvitationIn
logger.info("--- TESZT 1: LÉTEZŐ FELHASZNÁLÓ MEGHÍVÁSA ---")
try:
res1 = await invite_to_organization(
org_id=org_id,
invite_in=OrgInvitationIn(email=email_existing, role="DRIVER"),
db=db,
current_user=owner_user
)
logger.info(f"Teszt 1 Eredmény: {res1}")
stmt_mem = select(OrganizationMember).where(OrganizationMember.user_id == existing_user.id)
mem = (await db.execute(stmt_mem)).scalar_one()
assert mem.status == "pending"
assert mem.role == "DRIVER"
logger.info("Teszt 1 SIKERES")
except Exception as e:
logger.error(f"Teszt 1 HIBA: {e}")
logger.info("--- TESZT 2: ÚJ FELHASZNÁLÓ MEGHÍVÁSA ---")
try:
res2 = await invite_to_organization(
org_id=org_id,
invite_in=OrgInvitationIn(email=email_newbie, role="MECHANIC"),
db=db,
current_user=owner_user
)
logger.info(f"Teszt 2 Eredmény: {res2}")
stmt_token = select(VerificationToken).where(VerificationToken.token_type == "org_invite")
tokens = (await db.execute(stmt_token)).scalars().all()
token_rec = next(t for t in tokens if t.extra_data and t.extra_data.get("email") == email_newbie)
assert token_rec.extra_data["role"] == "MECHANIC"
assert token_rec.extra_data["org_id"] == org_id
newbie_in = UserLiteRegister(
email=email_newbie, password="Password123!", first_name="Újonc", last_name="Teszt", region_code="HU", lang="hu"
)
newbie_user = await AuthService.register_lite(db, newbie_in)
await db.commit()
res3 = await accept_invitation(token=token_rec.token, db=db, current_user=newbie_user)
logger.info(f"Teszt 2 Elfogadás: {res3}")
stmt_mem2 = select(OrganizationMember).where(OrganizationMember.user_id == newbie_user.id)
mem2 = (await db.execute(stmt_mem2)).scalar_one()
assert mem2.status == "active"
logger.info("Teszt 2 SIKERES")
except Exception as e:
logger.error(f"Teszt 2 HIBA: {e}")
logger.info("--- TESZT 3: SHADOW IDENTITY CHECK ---")
try:
shadow_person = Person(
first_name="Elek",
last_name=shadow_name,
birth_date=date(1990, 5, 5),
is_active=False,
is_ghost=True,
identity_docs={"ocr": "done"},
ice_contact={"name": "Teszt", "phone": "123", "relationship": "barat"}
)
db.add(shadow_person)
await db.commit()
shadow_person_id = shadow_person.id
logger.info(f"Létrehozott Shadow Person ID: {shadow_person_id}")
shadow_user_in = UserLiteRegister(
email=email_shadow, password="Password123!", first_name="Elek", last_name=shadow_name, region_code="HU", lang="hu"
)
shadow_user = await AuthService.register_lite(db, shadow_user_in)
shadow_user.is_active = True
await db.commit()
eredeti_person_id = shadow_user.person_id
kyc_shadow = UserKYCComplete(
phone_number="+36309999999",
birth_place="Debrecen",
birth_date=date(1990, 5, 5),
mothers_last_name="Teszt",
mothers_first_name="Anya",
address_zip="4000",
address_city="Debrecen",
address_street_name="",
address_street_type="utca",
address_house_number="2",
identity_docs={"manual": "done"},
ice_contact={"name": "Teszt", "phone": "123", "relationship": "barat"}
)
updated_user = await AuthService.complete_kyc(db, shadow_user.id, kyc_shadow)
logger.info(f"Eredeti Person ID: {eredeti_person_id}, Új Person ID: {updated_user.person_id}")
if updated_user.person_id == shadow_person_id:
logger.info("Teszt 3 SIKERES")
else:
logger.error("Teszt 3 SIKERTELEN (Nem az árnyék ID-t kapta)")
except Exception as e:
logger.error(f"Teszt 3 HIBA: {e}")
if __name__ == "__main__":
asyncio.run(run_tests())

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Ellenőrzi, hogy a #179-es issue séma változtatásai sikeresen alkalmazva lettek-e.
- branch_id és relocation_performed oszlopok az assets táblában
- verified_purchase_date oszlop az asset_financials táblában
"""
import asyncio
import sys
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@postgres:5432/service_finder"
async def check_columns():
"""Ellenőrzi a hiányzó oszlopokat."""
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# 1. assets tábla oszlopai
result = await session.execute(text("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'assets'
AND column_name IN ('branch_id', 'relocation_performed')
ORDER BY column_name;
"""))
assets_cols = {row[0]: row for row in result.fetchall()}
# 2. asset_financials tábla oszlopai
result = await session.execute(text("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'asset_financials'
AND column_name = 'verified_purchase_date';
"""))
financials_cols = {row[0]: row for row in result.fetchall()}
print("=== #179 SCHEMA ELLENŐRZÉS ===")
print("1. vehicle.assets tábla:")
if 'branch_id' in assets_cols:
col = assets_cols['branch_id']
print(f" ✅ branch_id: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ branch_id oszlop HIÁNYZIK!")
if 'relocation_performed' in assets_cols:
col = assets_cols['relocation_performed']
print(f" ✅ relocation_performed: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ relocation_performed oszlop HIÁNYZIK!")
print("\n2. vehicle.asset_financials tábla:")
if 'verified_purchase_date' in financials_cols:
col = financials_cols['verified_purchase_date']
print(f" ✅ verified_purchase_date: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ verified_purchase_date oszlop HIÁNYZIK!")
# 3. Foreign key ellenőrzés (opcionális)
result = await session.execute(text("""
SELECT tc.constraint_name, tc.constraint_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'vehicle' AND tc.table_name = 'assets'
AND kcu.column_name = 'branch_id'
AND tc.constraint_type = 'FOREIGN KEY';
"""))
fk = result.fetchone()
if fk:
print(f"\n3. Foreign key meglétének ellenőrzése: ✅ {fk[0]}")
else:
print("\n3. Foreign key meglétének ellenőrzése: ⚠️ Nincs foreign key constraint (lehet, hogy nem kötelező)")
# 4. Default érték ellenőrzés relocation_performed-re
result = await session.execute(text("""
SELECT column_default
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'assets'
AND column_name = 'relocation_performed';
"""))
default = result.scalar()
if default and 'false' in default.lower():
print(f"4. Default érték relocation_performed: ✅ {default}")
else:
print(f"4. Default érték relocation_performed: ⚠️ {default}")
# Összegzés
missing = []
if 'branch_id' not in assets_cols:
missing.append('branch_id')
if 'relocation_performed' not in assets_cols:
missing.append('relocation_performed')
if 'verified_purchase_date' not in financials_cols:
missing.append('verified_purchase_date')
if not missing:
print("\n🎉 ÖSSZES SÉMA VÁLTOZTATÁS SIKERESEN ALKALMAZVA!")
return True
else:
print(f"\n❌ HIÁNYZÓ OSZLOPOK: {', '.join(missing)}")
return False
async def main():
try:
success = await check_columns()
sys.exit(0 if success else 1)
except Exception as e:
print(f"Hiba történt: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""
Trigger Auth Emails — Diagnostic Script
=========================================
Szimulálja az éles API végpontok működését a teljes Auth logikán keresztül,
hogy kiderüljön, hol akad el a folyamat.
Használat:
docker compose exec sf_api python3 /app/trigger_auth_emails.py
Környezeti változók:
- TEST_EMAIL: Cél e-mail cím (alapértelmezett: accipe.t@aplast.hu)
- EMAIL_PROVIDER: smtp | brevo_api | brevo_smtp
"""
import os
import sys
import asyncio
import logging
# Ensure the backend package is importable
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# ── Load .env ──
from dotenv import load_dotenv
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env')
load_dotenv(env_path, override=False)
# ── Logging setup ──
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger("TriggerAuthEmails")
# Increase log level for noisy libs
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
async def main():
print("=" * 70)
print(" SERVICE FINDER — TRIGGER AUTH EMAILS (DIAGNOSTIC)")
print("=" * 70)
# ── 1. Target email ──
target_email = os.getenv("TEST_EMAIL", "accipe.t@aplast.hu")
print(f"\n🎯 Target email: {target_email}")
# ── 2. Show email config ──
provider = os.getenv("EMAIL_PROVIDER", "smtp")
print(f"\n📧 Current EMAIL_PROVIDER: {provider}")
print(f" SMTP_HOST: {os.getenv('SMTP_HOST', '(not set)')}")
print(f" SMTP_PORT: {os.getenv('SMTP_PORT', '(not set)')}")
print(f" MAIL_FROM: {os.getenv('MAIL_FROM', '(not set)')}")
print(f" MAIL_FROM_NAME: {os.getenv('MAIL_FROM_NAME', '(not set)')}")
print(f" BREVO_API_KEY: {'✅ Set' if os.getenv('BREVO_API_KEY') else '❌ NOT SET'}")
print(f" BREVO_SMTP_USER: {'✅ Set' if os.getenv('BREVO_SMTP_USER') else '❌ NOT SET'}")
print(f" BREVO_SMTP_PASSWORD: {'✅ Set' if os.getenv('BREVO_SMTP_PASSWORD') else '❌ NOT SET'}")
# ── 3. Import and execute ──
try:
from app.db.session import AsyncSessionLocal
from app.services.auth_service import AuthService
from sqlalchemy import text
# ── 3a. Check if user exists ──
print(f"\n🔍 Checking user: {target_email}...")
async with AsyncSessionLocal() as db:
result = await db.execute(
text("SELECT id, email, is_active, preferred_language, region_code FROM identity.users WHERE email = :email AND is_deleted = false"),
{"email": target_email}
)
user = result.fetchone()
if not user:
print(f"❌ User not found with email: {target_email}")
print(" Check if the user exists in the database.")
return
user_id, email, is_active, lang, region = user
print(f" ✅ User found:")
print(f" ID: {user_id}")
print(f" Email: {email}")
print(f" Active: {is_active}")
print(f" Lang: {lang}")
print(f" Region: {region}")
# ── 3b. Check existing verification tokens ──
result = await db.execute(
text("""
SELECT id, token, token_type, is_used, expires_at, created_at
FROM identity.verification_tokens
WHERE user_id = :uid
ORDER BY created_at DESC
LIMIT 5
"""),
{"uid": user_id}
)
tokens = result.fetchall()
print(f"\n🔑 Existing verification tokens for user #{user_id}:")
if tokens:
for t in tokens:
print(f" ID={t[0]} type={t[2]} used={t[3]} expires={t[4]} created={t[5]}")
else:
print(" (none)")
# ── 3c. Test 1: Resend Verification ──
print(f"\n{'=' * 70}")
print(f" 📬 TEST 1: Resend Verification Email")
print(f"{'=' * 70}")
print(f" Calling AuthService.resend_verification(email='{target_email}')...\n")
async with AsyncSessionLocal() as db:
try:
result = await AuthService.resend_verification(db=db, email=target_email)
print(f"\n✅ Result: {result}")
except Exception as e:
print(f"\n❌ Exception: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
# ── 3d. Test 2: Initiate Password Reset ──
print(f"\n{'=' * 70}")
print(f" 📬 TEST 2: Initiate Password Reset Email")
print(f"{'=' * 70}")
print(f" Calling AuthService.initiate_password_reset(email='{target_email}')...\n")
async with AsyncSessionLocal() as db:
try:
result = await AuthService.initiate_password_reset(db=db, email=target_email)
print(f"\n✅ Result: {result}")
except Exception as e:
print(f"\n❌ Exception: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
# ── 3e. Test 3: Direct EmailManager call ──
print(f"\n{'=' * 70}")
print(f" 📬 TEST 3: Direct EmailManager.send_email (template_key='reg')")
print(f"{'=' * 70}")
print(f" Calling EmailManager.send_email() directly...\n")
from app.services.email_manager import EmailManager
async with AsyncSessionLocal() as db:
try:
result = await EmailManager.send_email(
recipient=target_email,
template_key="reg",
variables={
"first_name": "Teszt",
"link": "https://dev.servicefinder.hu/verify?token=test-token-123",
},
lang="hu",
db=db,
)
print(f"\n✅ Result: {result}")
except Exception as e:
print(f"\n❌ Exception: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
# ── 3f. Test 4: Direct EmailManager call with pwd_reset template ──
print(f"\n{'=' * 70}")
print(f" 📬 TEST 4: Direct EmailManager.send_email (template_key='pwd_reset')")
print(f"{'=' * 70}")
print(f" Calling EmailManager.send_email() directly...\n")
async with AsyncSessionLocal() as db:
try:
result = await EmailManager.send_email(
recipient=target_email,
template_key="pwd_reset",
variables={
"link": "https://dev.servicefinder.hu/reset-password?token=test-token-456",
},
lang="hu",
db=db,
)
print(f"\n✅ Result: {result}")
except Exception as e:
print(f"\n❌ Exception: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
except ImportError as e:
print(f"\n❌ Import Error: {e}")
print(" Make sure you run this inside the sf_api container!")
print(" Command: docker compose exec sf_api python3 /app/trigger_auth_emails.py")
sys.exit(1)
print(f"\n{'=' * 70}")
print(" ✅ DIAGNOSTIC COMPLETE")
print(f"{'=' * 70}")
if __name__ == "__main__":
asyncio.run(main())