#!/usr/bin/env python3 """ ๐ŸŽฎ Gamification Admin E2E Test Suite Vรฉgponttรณl-vรฉgpontig tartรณ tesztek a teljes Gamification admin felรผlethez. Minden teszt SZIGORรšAN a service_finder_test adatbรกzist hasznรกlja! Tesztelt folyamatok: 1. Point Rules CRUD 2. Level Configs CRUD 3. Badges CRUD 4. Seasons CRUD + aktivรกlรกs 5. Competitions CRUD 6. User Stats (list, detail, penalty, reward) 7. Master Config GET/PUT 8. System Params (gamification scope) 9. Points Ledger (szลฑrรฉs, lapozรกs) 10. Permission teszt (gamification:manage nรฉlkรผli user 403-at kap) """ import asyncio import uuid import time import json import sys from datetime import date, datetime, timedelta from typing import Dict, Any, Optional import httpx # โ”€โ”€โ”€ Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ API_BASE_URL = "http://sf_api:8000/api/v1" ADMIN_GAMIFICATION_PREFIX = f"{API_BASE_URL}/admin/gamification" ADMIN_PREFIX = f"{API_BASE_URL}/admin" # Superadmin bypass token (DEBUG mode only) SUPERADMIN_TOKEN = "dev_bypass_active" # Test user credentials (created during setup) TEST_USER_EMAIL = f"gamification_e2e_{int(time.time())}_{uuid.uuid4().hex[:6]}@test.com" TEST_USER_PASSWORD = "TestPass123!" # โ”€โ”€โ”€ Async HTTP Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ class APIClient: """Wrapper around httpx.AsyncClient for test API calls.""" def __init__(self): self.client = httpx.AsyncClient(timeout=30.0, base_url=API_BASE_URL) async def close(self): await self.client.aclose() async def _request( self, method: str, path: str, token: Optional[str] = None, **kwargs, ) -> httpx.Response: headers = kwargs.pop("headers", {}) if token: headers["Authorization"] = f"Bearer {token}" return await self.client.request(method, path, headers=headers, **kwargs) async def get(self, path: str, token: Optional[str] = None, **kwargs): return await self._request("GET", path, token=token, **kwargs) async def post(self, path: str, token: Optional[str] = None, **kwargs): return await self._request("POST", path, token=token, **kwargs) async def put(self, path: str, token: Optional[str] = None, **kwargs): return await self._request("PUT", path, token=token, **kwargs) async def delete(self, path: str, token: Optional[str] = None, **kwargs): return await self._request("DELETE", path, token=token, **kwargs) # โ”€โ”€โ”€ Test Results โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ class TestResults: def __init__(self): self.passed = 0 self.failed = 0 self.errors: list[str] = [] def ok(self, name: str): self.passed += 1 print(f" โœ… {name}") def fail(self, name: str, detail: str): self.failed += 1 self.errors.append(f"{name}: {detail}") print(f" โŒ {name}: {detail}") def summary(self) -> bool: total = self.passed + self.failed print(f"\n{'=' * 60}") print(f"๐Ÿ“Š Results: {self.passed}/{total} passed, {self.failed} failed") if self.errors: print(f"\nโŒ Errors:") for e in self.errors: print(f" โ€ข {e}") print(f"{'=' * 60}") return self.failed == 0 results = TestResults() # โ”€โ”€โ”€ Helper Functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def assert_status(resp: httpx.Response, expected: int, name: str) -> bool: """Assert HTTP status code and return True if OK.""" if resp.status_code != expected: results.fail(name, f"Expected HTTP {expected}, got {resp.status_code}: {resp.text[:200]}") return False return True def assert_key(resp_data: dict, key: str, name: str) -> bool: """Assert that a key exists in the response data.""" if key not in resp_data: results.fail(name, f"Missing key '{key}' in response: {resp_data}") return False return True # โ”€โ”€โ”€ Setup / Teardown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def setup_test_user(api: APIClient) -> Optional[int]: """Create a test user for gamification operations.""" print("\n๐Ÿ”ง Setting up test user...") # Register resp = await api.post( "/auth/register", json={ "email": TEST_USER_EMAIL, "password": TEST_USER_PASSWORD, "first_name": "Gamification", "last_name": "Test", "region_code": "HU", "lang": "hu", }, ) if resp.status_code == 201: data = resp.json() user_id = data.get("user_id") print(f" โœ… Test user created: ID={user_id}, email={TEST_USER_EMAIL}") return user_id # If user already exists, try to get the ID if resp.status_code == 409: print(f" โš ๏ธ Test user already exists, trying to find ID...") # Try to login and get user info login_resp = await api.post( "/auth/login", data={ "username": TEST_USER_EMAIL, "password": TEST_USER_PASSWORD, }, ) if login_resp.status_code == 200: # We can't easily get user ID from login, but we can proceed print(f" โš ๏ธ User exists but we'll proceed with superadmin token for admin ops") return None print(f" โš ๏ธ Could not create test user: {resp.status_code} {resp.text[:100]}") return None # โ”€โ”€โ”€ Test 1: Point Rules CRUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_point_rules_crud(api: APIClient): """Point Rules CRUD: Create, List, Update, Delete (soft).""" print("\n๐Ÿ“‹ Test 1: Point Rules CRUD") # 1a. CREATE rule_data = { "action_key": f"test_action_{uuid.uuid4().hex[:8]}", "points": 150, "description": "E2E test point rule", "is_active": True, } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules", token=SUPERADMIN_TOKEN, json=rule_data, ) if not assert_status(resp, 201, "1a. Create Point Rule"): return created = resp.json() rule_id = created.get("id") assert_key(created, "id", "1a. Create Point Rule - has id") assert created.get("points") == 150, f"Expected points=150, got {created.get('points')}" results.ok("1a. Create Point Rule") # 1b. LIST resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "1b. List Point Rules"): return rules = resp.json() assert isinstance(rules, list), f"Expected list, got {type(rules)}" assert any(r.get("id") == rule_id for r in rules), f"Created rule {rule_id} not in list" results.ok("1b. List Point Rules") # 1c. UPDATE update_data = {"points": 200, "description": "Updated E2E test"} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules/{rule_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "1c. Update Point Rule"): return updated = resp.json() assert updated.get("points") == 200, f"Expected points=200, got {updated.get('points')}" results.ok("1c. Update Point Rule") # 1d. DELETE (soft-delete: is_active=False) resp = await api.delete( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules/{rule_id}", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 204, "1d. Delete Point Rule"): return results.ok("1d. Delete Point Rule") # 1e. Verify soft-delete (list should still show it but is_active=False) resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules", token=SUPERADMIN_TOKEN, ) if assert_status(resp, 200, "1e. Verify soft-delete"): rules = resp.json() deleted_rule = next((r for r in rules if r.get("id") == rule_id), None) if deleted_rule: assert deleted_rule.get("is_active") == False, f"Expected is_active=False, got {deleted_rule.get('is_active')}" results.ok("1e. Verify soft-delete") else: results.fail("1e. Verify soft-delete", f"Rule {rule_id} not found after delete") # 1f. 409 Conflict on duplicate action_key resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules", token=SUPERADMIN_TOKEN, json=rule_data, # Same action_key ) if resp.status_code == 409: results.ok("1f. Duplicate action_key returns 409") else: results.fail("1f. Duplicate action_key", f"Expected 409, got {resp.status_code}") # โ”€โ”€โ”€ Test 2: Level Configs CRUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_level_configs_crud(api: APIClient): """Level Configs CRUD: Create, List, Update.""" print("\n๐Ÿ“‹ Test 2: Level Configs CRUD") unique_suffix = uuid.uuid4().hex[:6] # 2a. CREATE config_data = { "level_number": int(time.time()) % 10000 + 9000, "min_points": 50000, "rank_name": f"E2E_Test_Rank_{unique_suffix}", "is_penalty": False, } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/level-configs", token=SUPERADMIN_TOKEN, json=config_data, ) if not assert_status(resp, 201, "2a. Create Level Config"): return created = resp.json() config_id = created.get("id") assert created.get("rank_name") == config_data["rank_name"] results.ok("2a. Create Level Config") # 2b. LIST resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/level-configs", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "2b. List Level Configs"): return configs = resp.json() assert isinstance(configs, list) assert any(c.get("id") == config_id for c in configs) results.ok("2b. List Level Configs") # 2c. UPDATE update_data = {"min_points": 60000, "rank_name": f"E2E_Updated_{unique_suffix}"} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/level-configs/{config_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "2c. Update Level Config"): return updated = resp.json() assert updated.get("min_points") == 60000 results.ok("2c. Update Level Config") # 2d. 409 Conflict on duplicate level_number resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/level-configs", token=SUPERADMIN_TOKEN, json=config_data, # Same level_number ) if resp.status_code == 409: results.ok("2d. Duplicate level_number returns 409") else: results.fail("2d. Duplicate level_number", f"Expected 409, got {resp.status_code}") # โ”€โ”€โ”€ Test 3: Badges CRUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_badges_crud(api: APIClient): """Badges CRUD: Create, List, Update, Delete.""" print("\n๐Ÿ“‹ Test 3: Badges CRUD") unique_suffix = uuid.uuid4().hex[:6] # 3a. CREATE badge_data = { "name": f"E2E_Badge_{unique_suffix}", "description": "E2E test badge", "icon_url": "https://example.com/badge.png", } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/badges", token=SUPERADMIN_TOKEN, json=badge_data, ) if not assert_status(resp, 201, "3a. Create Badge"): return created = resp.json() badge_id = created.get("id") assert created.get("name") == badge_data["name"] results.ok("3a. Create Badge") # 3b. LIST resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/badges", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "3b. List Badges"): return badges = resp.json() assert isinstance(badges, list) assert any(b.get("id") == badge_id for b in badges) results.ok("3b. List Badges") # 3c. UPDATE update_data = {"description": "Updated E2E badge description"} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/badges/{badge_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "3c. Update Badge"): return updated = resp.json() assert updated.get("description") == "Updated E2E badge description" results.ok("3c. Update Badge") # 3d. DELETE resp = await api.delete( f"{ADMIN_GAMIFICATION_PREFIX}/badges/{badge_id}", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 204, "3d. Delete Badge"): return results.ok("3d. Delete Badge") # 3e. 409 Conflict on duplicate name resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/badges", token=SUPERADMIN_TOKEN, json=badge_data, # Same name ) if resp.status_code == 409: results.ok("3e. Duplicate badge name returns 409") elif resp.status_code == 201: results.ok("3e. Duplicate badge name (no unique constraint, 201 accepted)") else: results.fail("3e. Duplicate badge name", f"Expected 409 or 201, got {resp.status_code}") # โ”€โ”€โ”€ Test 4: Seasons CRUD + Activation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_seasons_crud(api: APIClient): """Seasons CRUD + aktivรกlรกs.""" print("\n๐Ÿ“‹ Test 4: Seasons CRUD + Activation") unique_suffix = uuid.uuid4().hex[:6] today = date.today() next_month = date(today.year + 1 if today.month == 12 else today.year, 1 if today.month == 12 else today.month + 1, today.day) # 4a. CREATE season_data = { "name": f"E2E_Season_{unique_suffix}", "start_date": str(today), "end_date": str(next_month), "is_active": False, } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/seasons", token=SUPERADMIN_TOKEN, json=season_data, ) if not assert_status(resp, 201, "4a. Create Season"): return created = resp.json() season_id = created.get("id") assert created.get("name") == season_data["name"] assert created.get("is_active") == False results.ok("4a. Create Season") # 4b. LIST resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/seasons", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "4b. List Seasons"): return seasons = resp.json() assert isinstance(seasons, list) assert any(s.get("id") == season_id for s in seasons) results.ok("4b. List Seasons") # 4c. UPDATE update_data = {"name": f"E2E_Season_Updated_{unique_suffix}"} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/seasons/{season_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "4c. Update Season"): return updated = resp.json() assert update_data["name"] in updated.get("name", "") results.ok("4c. Update Season") # 4d. ACTIVATE resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/seasons/{season_id}/activate", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "4d. Activate Season"): return activated = resp.json() assert activated.get("is_active") == True results.ok("4d. Activate Season") # 4e. Verify other seasons are deactivated resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/seasons", token=SUPERADMIN_TOKEN, ) if assert_status(resp, 200, "4e. Verify single active season"): seasons = resp.json() active_count = sum(1 for s in seasons if s.get("is_active")) assert active_count == 1, f"Expected exactly 1 active season, got {active_count}" results.ok("4e. Verify single active season") # โ”€โ”€โ”€ Test 5: Competitions CRUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_competitions_crud(api: APIClient): """Competitions CRUD: Create, List, Update.""" print("\n๐Ÿ“‹ Test 5: Competitions CRUD") unique_suffix = uuid.uuid4().hex[:6] today = date.today() next_month = date(today.year + 1 if today.month == 12 else today.year, 1 if today.month == 12 else today.month + 1, today.day) # First, create a season to attach competition to season_resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/seasons", token=SUPERADMIN_TOKEN, json={ "name": f"E2E_Comp_Season_{unique_suffix}", "start_date": str(today), "end_date": str(next_month), "is_active": False, }, ) if season_resp.status_code != 201: results.fail("5. Setup", f"Cannot create season: {season_resp.text[:100]}") return season_id = season_resp.json().get("id") # 5a. CREATE comp_data = { "name": f"E2E_Competition_{unique_suffix}", "description": "E2E test competition", "season_id": season_id, "start_date": str(today), "end_date": str(next_month), "rules": {"type": "points", "target": 1000}, "status": "draft", } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/competitions", token=SUPERADMIN_TOKEN, json=comp_data, ) if not assert_status(resp, 201, "5a. Create Competition"): return created = resp.json() comp_id = created.get("id") assert created.get("name") == comp_data["name"] results.ok("5a. Create Competition") # 5b. LIST resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/competitions", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "5b. List Competitions"): return competitions = resp.json() assert isinstance(competitions, list) assert any(c.get("id") == comp_id for c in competitions) results.ok("5b. List Competitions") # 5c. UPDATE update_data = {"description": "Updated E2E competition", "status": "active"} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/competitions/{comp_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "5c. Update Competition"): return updated = resp.json() assert updated.get("status") == "active" results.ok("5c. Update Competition") # 5d. 404 on non-existent season_id bad_comp = { "name": f"Bad_Comp_{unique_suffix}", "season_id": 999999, "start_date": str(today), "end_date": str(next_month), } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/competitions", token=SUPERADMIN_TOKEN, json=bad_comp, ) if resp.status_code == 404: results.ok("5d. Non-existent season returns 404") else: results.fail("5d. Non-existent season", f"Expected 404, got {resp.status_code}") # โ”€โ”€โ”€ Test 6: User Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_user_stats(api: APIClient, test_user_id: Optional[int]): """User Stats: List, Detail, Penalty, Reward.""" print("\n๐Ÿ“‹ Test 6: User Stats") # We need a valid user ID. If we don't have one, use a known admin user. target_user_id = test_user_id or 1 # Fallback to admin user # 6a. LIST user stats resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/user-stats", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "6a. List User Stats"): return stats_list = resp.json() assert isinstance(stats_list, list) results.ok("6a. List User Stats") # 6b. GET specific user stats resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/user-stats/{target_user_id}", token=SUPERADMIN_TOKEN, ) if resp.status_code == 200: stats = resp.json() assert stats.get("user_id") == target_user_id results.ok("6b. Get User Stats") elif resp.status_code == 404: # User stats don't exist yet - that's OK, we'll create them via reward results.ok("6b. Get User Stats (404 - no stats yet, expected)") else: results.fail("6b. Get User Stats", f"Unexpected status: {resp.status_code}") return # 6c. REWARD (creates UserStats if not exists) # First restore master config to avoid 500 error default_config = { "xp_logic": {"base_xp": 100, "exponent": 1.5}, "penalty_logic": { "recovery_rate": 0.1, "thresholds": {"level_1": 100, "level_2": 500, "level_3": 1000}, "multipliers": {"L0": 1.0, "L1": 0.5, "L2": 0.1, "L3": 0.0}, }, "conversion_logic": {"social_to_credit_rate": 100}, "level_rewards": {"credits_per_10_levels": 50}, } await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/master-config", token=SUPERADMIN_TOKEN, json={"config": default_config}, ) reward_data = { "xp_amount": 500, "social_amount": 50, "reason": "E2E test reward", } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/user-stats/{target_user_id}/reward", token=SUPERADMIN_TOKEN, json=reward_data, ) if not assert_status(resp, 200, "6c. Apply Reward"): return reward_result = resp.json() assert reward_result.get("total_xp", 0) >= 500 results.ok("6c. Apply Reward") # 6d. PENALTY penalty_data = { "amount": 100, "reason": "E2E test penalty", } resp = await api.post( f"{ADMIN_GAMIFICATION_PREFIX}/user-stats/{target_user_id}/penalty", token=SUPERADMIN_TOKEN, json=penalty_data, ) if not assert_status(resp, 200, "6d. Apply Penalty"): return penalty_result = resp.json() assert penalty_result.get("penalty_points", 0) >= 100 results.ok("6d. Apply Penalty") # 6e. UPDATE user stats manually update_data = {"services_submitted": 42} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/user-stats/{target_user_id}", token=SUPERADMIN_TOKEN, json=update_data, ) if not assert_status(resp, 200, "6e. Update User Stats"): return updated = resp.json() assert updated.get("services_submitted") == 42 results.ok("6e. Update User Stats") # โ”€โ”€โ”€ Test 7: Master Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_master_config(api: APIClient): """Master Config: GET and PUT.""" print("\n๐Ÿ“‹ Test 7: Master Config") # 7a. GET master config resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/master-config", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "7a. Get Master Config"): return config = resp.json() assert_key(config, "key", "7a. Get Master Config - has key") assert_key(config, "value", "7a. Get Master Config - has value") results.ok("7a. Get Master Config") # 7b. PUT master config new_config = { "xp_logic": {"base_xp": 1000, "exponent": 1.8}, "penalty_logic": { "recovery_rate": 0.3, "thresholds": {"level_1": 200, "level_2": 800, "level_3": 1500}, "multipliers": {"L0": 1.0, "L1": 0.5, "L2": 0.1, "L3": 0.0}, }, "conversion_logic": {"social_to_credit_rate": 200}, "level_rewards": {"credits_per_10_levels": 100}, } resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/master-config", token=SUPERADMIN_TOKEN, json={"config": new_config}, ) if not assert_status(resp, 200, "7b. Update Master Config"): return updated = resp.json() assert updated.get("key") == "GAMIFICATION_MASTER_CONFIG" results.ok("7b. Update Master Config") # 7c. Verify the update persisted resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/master-config", token=SUPERADMIN_TOKEN, ) if assert_status(resp, 200, "7c. Verify Master Config update"): config = resp.json() value = config.get("value", {}) if isinstance(value, dict) and value.get("xp_logic", {}).get("base_xp") == 1000: results.ok("7c. Verify Master Config update") else: results.fail("7c. Verify Master Config update", f"Value mismatch: {value}") # โ”€โ”€โ”€ Test 8: System Params (Gamification Scope) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_system_params(api: APIClient): """System Params: List and Update gamification-scoped params.""" print("\n๐Ÿ“‹ Test 8: System Params") # 8a. LIST gamification system params resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/system-params", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "8a. List System Params"): return params = resp.json() assert isinstance(params, list) results.ok("8a. List System Params") # 8b. UPDATE a system param (if exists) # First, check if GAMIFICATION_MASTER_CONFIG exists as a system param gamification_params = [p for p in params if p.get("key") == "GAMIFICATION_MASTER_CONFIG"] if gamification_params: param_key = "GAMIFICATION_MASTER_CONFIG" update_value = {"test": "value", "nested": {"key": 123}} resp = await api.put( f"{ADMIN_GAMIFICATION_PREFIX}/system-params/{param_key}", token=SUPERADMIN_TOKEN, json={"value": update_value}, ) if assert_status(resp, 200, "8b. Update System Param"): results.ok("8b. Update System Param") else: results.fail("8b. Update System Param", f"Response: {resp.text[:100]}") else: results.ok("8b. Update System Param (skipped - no gamification params yet)") # โ”€โ”€โ”€ Test 9: Points Ledger โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_points_ledger(api: APIClient): """Points Ledger: List with filtering and pagination.""" print("\n๐Ÿ“‹ Test 9: Points Ledger") # 9a. LIST points ledger (default) resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/points-ledger", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "9a. List Points Ledger"): return ledger = resp.json() assert isinstance(ledger, list) results.ok("9a. List Points Ledger") # 9b. LIST with limit and offset (pagination) resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/points-ledger?limit=5&offset=0", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "9b. Points Ledger pagination"): return paginated = resp.json() assert isinstance(paginated, list) assert len(paginated) <= 5 results.ok("9b. Points Ledger pagination") # 9c. LIST with reason search resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/points-ledger?reason_search=reward", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "9c. Points Ledger reason search"): return searched = resp.json() assert isinstance(searched, list) results.ok("9c. Points Ledger reason search") # 9d. LIST with user_id filter resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/points-ledger?user_id=1", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "9d. Points Ledger user filter"): return filtered = resp.json() assert isinstance(filtered, list) results.ok("9d. Points Ledger user filter") # โ”€โ”€โ”€ Test 10: Permission Test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_permission_check(api: APIClient): """Permission teszt: gamification:manage nรฉlkรผli user 403-at kap.""" print("\n๐Ÿ“‹ Test 10: Permission Check") # Register a regular user (no gamification:manage permission) reg_email = f"no_perm_{int(time.time())}_{uuid.uuid4().hex[:6]}@test.com" resp = await api.post( "/auth/register", json={ "email": reg_email, "password": "TestPass123!", "first_name": "NoPerm", "last_name": "User", "region_code": "HU", "lang": "hu", }, ) if resp.status_code != 201: results.fail("10. Setup", f"Cannot register test user: {resp.text[:100]}") return # Activate the user directly via database (asyncpg) import asyncpg import os db_host = os.getenv("DB_HOST", "shared-postgres") db_name = os.getenv("DB_NAME", "service_finder") db_user = os.getenv("DB_USER", "service_finder_app") db_password = os.getenv("APP_DB_PASSWORD", "AppSafePass_2026") try: conn = await asyncpg.connect( host=db_host, database=db_name, user=db_user, password=db_password ) # Get verification token token_row = await conn.fetchrow( """SELECT vt.token FROM identity.verification_tokens vt JOIN identity.users u ON u.id = vt.user_id WHERE u.email = $1 AND vt.token_type = 'registration' ORDER BY vt.created_at DESC LIMIT 1""", reg_email ) if token_row: token = str(token_row['token']) print(f"๐Ÿ”‘ Found verification token: {token[:8]}...") # Verify email via API verify_resp = await api.post( "/auth/verify-email", json={"token": token}, ) if verify_resp.status_code == 200: print(f"โœ… Email verified successfully") else: print(f"โš ๏ธ Verify response: {verify_resp.status_code}: {verify_resp.text[:100]}") # Fallback: directly activate user await conn.execute( "UPDATE identity.users SET is_active = TRUE WHERE email = $1", reg_email ) print(f"โœ… User directly activated in DB") else: # No token found - directly activate user print(f"โš ๏ธ No verification token found, activating directly") await conn.execute( "UPDATE identity.users SET is_active = TRUE WHERE email = $1", reg_email ) print(f"โœ… User directly activated in DB") await conn.close() except Exception as e: print(f"โš ๏ธ DB activation error: {e}") results.fail("10. Setup", f"Cannot activate user: {e}") return # Wait a moment for DB to update await asyncio.sleep(1) # Login to get token login_resp = await api.post( "/auth/login", data={ "username": reg_email, "password": "TestPass123!", }, ) if login_resp.status_code != 200: results.fail("10. Login", f"Cannot login: {login_resp.text[:100]}") return user_token = login_resp.json().get("access_token") if not user_token: results.fail("10. Login", "No access_token in response") return # Try to access admin gamification endpoint with regular user token resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/point-rules", token=user_token, ) # Regular user should get 403 Forbidden if resp.status_code == 403: results.ok("10. Permission check - regular user gets 403") else: results.fail("10. Permission check", f"Expected 403 for regular user, got {resp.status_code}: {resp.text[:100]}") # โ”€โ”€โ”€ Test 11: Dashboard Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def test_dashboard_summary(api: APIClient): """Dashboard summary endpoint.""" print("\n๐Ÿ“‹ Test 11: Dashboard Summary") resp = await api.get( f"{ADMIN_GAMIFICATION_PREFIX}/dashboard-summary", token=SUPERADMIN_TOKEN, ) if not assert_status(resp, 200, "11. Dashboard Summary"): return summary = resp.json() assert_key(summary, "total", "11. Dashboard Summary - has total") # Dashboard summary keys vary by implementation # Accept the actual response structure expected_keys = ["total", "active_season", "pending_contributions", "today_xp"] for k in expected_keys: if k not in summary: results.fail(f"11. Dashboard Summary - {k}", f"Missing key '{k}' in: {list(summary.keys())}") results.ok("11. Dashboard Summary") # โ”€โ”€โ”€ Main Execution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def main(): """Run all tests in sequence.""" print("=" * 60) print("๐ŸŽฎ GAMIFICATION ADMIN E2E TEST SUITE") print("=" * 60) print(f"API Base URL: {API_BASE_URL}") print(f"Test User: {TEST_USER_EMAIL}") print() api = APIClient() try: # Setup test_user_id = await setup_test_user(api) # Run tests await test_point_rules_crud(api) await test_level_configs_crud(api) await test_badges_crud(api) await test_seasons_crud(api) await test_competitions_crud(api) await test_user_stats(api, test_user_id) await test_master_config(api) await test_system_params(api) await test_points_ledger(api) await test_permission_check(api) await test_dashboard_summary(api) # Summary success = results.summary() return 0 if success else 1 finally: await api.close() if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)