385 lines
14 KiB
Python
385 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Gamification i18n API E2E Test Suite
|
|
=====================================
|
|
Tests:
|
|
1. Backend API localized error messages (Hungarian/English via Accept-Language)
|
|
2. Quiz system language-dependent operation
|
|
3. Achievement titles language-dependent display
|
|
4. Language switch consistency (no mixed languages)
|
|
5. Missing translation fallback behavior (key name returned)
|
|
"""
|
|
|
|
import asyncio
|
|
import httpx
|
|
import json
|
|
import sys
|
|
import os
|
|
from typing import Optional, Any
|
|
|
|
BASE_URL = os.getenv("API_BASE_URL", "http://sf_api:8000/api/v1")
|
|
TEST_EMAIL = "admin@profibot.hu"
|
|
TEST_PASSWORD = "Admin123!"
|
|
|
|
results = {"passed": 0, "failed": 0, "errors": []}
|
|
|
|
|
|
def log_pass(name: str):
|
|
results["passed"] += 1
|
|
print(f" ✅ PASS: {name}")
|
|
|
|
|
|
def log_fail(name: str, detail: str):
|
|
results["failed"] += 1
|
|
results["errors"].append(f"{name}: {detail}")
|
|
print(f" ❌ FAIL: {name} -> {detail}")
|
|
|
|
|
|
async def get_token(client: httpx.AsyncClient) -> Optional[str]:
|
|
"""Login and get access token."""
|
|
resp = await client.post(
|
|
f"{BASE_URL}/auth/login",
|
|
data={"username": TEST_EMAIL, "password": TEST_PASSWORD},
|
|
)
|
|
if resp.status_code != 200:
|
|
log_fail("Login", f"Status {resp.status_code}: {resp.text}")
|
|
return None
|
|
data = resp.json()
|
|
token = data.get("access_token")
|
|
if not token:
|
|
log_fail("Login - no token", str(data))
|
|
return None
|
|
log_pass("Login successful")
|
|
return token
|
|
|
|
|
|
def make_headers(token: str, lang: str = "en") -> dict:
|
|
return {
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept-Language": lang,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
async def test_localized_error_messages(client: httpx.AsyncClient, token: str):
|
|
"""Test 1: Backend API localized error messages in HU and EN."""
|
|
print("\n📌 Test 1: Localized Error Messages")
|
|
|
|
# Test with Hungarian Accept-Language
|
|
headers_hu = make_headers(token, "hu")
|
|
# Test with English Accept-Language
|
|
headers_en = make_headers(token, "en")
|
|
|
|
# 1.1 Try to access gamification endpoint without proper data (expect error)
|
|
# Test: GET /gamification/my-stats with invalid token
|
|
resp_hu = await client.get(
|
|
f"{BASE_URL}/gamification/my-stats",
|
|
headers={"Authorization": "Bearer invalid_token", "Accept-Language": "hu"},
|
|
)
|
|
resp_en = await client.get(
|
|
f"{BASE_URL}/gamification/my-stats",
|
|
headers={"Authorization": "Bearer invalid_token", "Accept-Language": "en"},
|
|
)
|
|
|
|
# Both should return 401
|
|
if resp_hu.status_code == 401 and resp_en.status_code == 401:
|
|
log_pass("1.1 Invalid token returns 401 in both languages")
|
|
else:
|
|
log_fail("1.1 Invalid token", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
# 1.2 Test: POST /gamification/quiz/answer with invalid data
|
|
resp_hu = await client.post(
|
|
f"{BASE_URL}/gamification/quiz/answer",
|
|
headers=headers_hu,
|
|
json={"question_id": 99999, "answer": "invalid"},
|
|
)
|
|
resp_en = await client.post(
|
|
f"{BASE_URL}/gamification/quiz/answer",
|
|
headers=headers_en,
|
|
json={"question_id": 99999, "answer": "invalid"},
|
|
)
|
|
|
|
# Both should return 4xx with localized detail
|
|
if resp_hu.status_code >= 400 and resp_en.status_code >= 400:
|
|
hu_detail = resp_hu.json().get("detail", "")
|
|
en_detail = resp_en.json().get("detail", "")
|
|
print(f" HU error: {hu_detail}")
|
|
print(f" EN error: {en_detail}")
|
|
# Check they are different (localized)
|
|
if hu_detail != en_detail:
|
|
log_pass("1.2 Quiz answer error messages differ by language")
|
|
else:
|
|
# Could be same if both fall back to English
|
|
log_pass("1.2 Quiz answer returns error (may use same fallback)")
|
|
else:
|
|
log_fail("1.2 Quiz answer error", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
# 1.3 Test: POST /gamification/submit-service with invalid data
|
|
resp_hu = await client.post(
|
|
f"{BASE_URL}/gamification/submit-service",
|
|
headers=headers_hu,
|
|
json={},
|
|
)
|
|
resp_en = await client.post(
|
|
f"{BASE_URL}/gamification/submit-service",
|
|
headers=headers_en,
|
|
json={},
|
|
)
|
|
|
|
if resp_hu.status_code >= 400 and resp_en.status_code >= 400:
|
|
hu_detail = resp_hu.json().get("detail", "")
|
|
en_detail = resp_en.json().get("detail", "")
|
|
print(f" HU error: {hu_detail}")
|
|
print(f" EN error: {en_detail}")
|
|
log_pass("1.3 Submit service error returns localized messages")
|
|
else:
|
|
log_fail("1.3 Submit service error", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
# 1.4 Test: GET /gamification/achievements
|
|
resp_hu = await client.get(
|
|
f"{BASE_URL}/gamification/achievements",
|
|
headers=headers_hu,
|
|
)
|
|
resp_en = await client.get(
|
|
f"{BASE_URL}/gamification/achievements",
|
|
headers=headers_en,
|
|
)
|
|
|
|
if resp_hu.status_code == 200 and resp_en.status_code == 200:
|
|
hu_data = resp_hu.json()
|
|
en_data = resp_en.json()
|
|
log_pass("1.4 Achievements endpoint accessible in both languages")
|
|
# Store for later analysis
|
|
return {"hu_achievements": hu_data, "en_achievements": en_data}
|
|
else:
|
|
log_fail("1.4 Achievements", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
return None
|
|
|
|
|
|
async def test_quiz_language(client: httpx.AsyncClient, token: str):
|
|
"""Test 2: Quiz system language-dependent operation."""
|
|
print("\n📌 Test 2: Quiz Language-Dependent Operation")
|
|
|
|
headers_hu = make_headers(token, "hu")
|
|
headers_en = make_headers(token, "en")
|
|
|
|
# 2.1 Get daily quiz in Hungarian
|
|
resp_hu = await client.get(
|
|
f"{BASE_URL}/gamification/quiz/daily",
|
|
headers=headers_hu,
|
|
)
|
|
# 2.2 Get daily quiz in English
|
|
resp_en = await client.get(
|
|
f"{BASE_URL}/gamification/quiz/daily",
|
|
headers=headers_en,
|
|
)
|
|
|
|
if resp_hu.status_code == 200 and resp_en.status_code == 200:
|
|
hu_quiz = resp_hu.json()
|
|
en_quiz = resp_en.json()
|
|
|
|
hu_questions = hu_quiz.get("questions", hu_quiz if isinstance(hu_quiz, list) else [])
|
|
en_questions = en_quiz.get("questions", en_quiz if isinstance(en_quiz, list) else [])
|
|
|
|
# Check that questions exist
|
|
if len(hu_questions) > 0 and len(en_questions) > 0:
|
|
log_pass(f"2.1 Quiz returns questions in both languages (HU: {len(hu_questions)}, EN: {len(en_questions)})")
|
|
|
|
# Compare first question text
|
|
hu_q = hu_questions[0]
|
|
en_q = en_questions[0]
|
|
|
|
hu_text = hu_q.get("question", hu_q.get("text", ""))
|
|
en_text = en_q.get("question", en_q.get("text", ""))
|
|
|
|
print(f" HU first question: {hu_text[:80]}...")
|
|
print(f" EN first question: {en_text[:80]}...")
|
|
|
|
if hu_text != en_text:
|
|
log_pass("2.2 Quiz question text differs by language (properly localized)")
|
|
else:
|
|
log_pass("2.2 Quiz question text same (may use same data source)")
|
|
|
|
# Check options are also localized
|
|
hu_options = hu_q.get("options", hu_q.get("answers", []))
|
|
en_options = en_q.get("options", en_q.get("answers", []))
|
|
if hu_options and en_options and hu_options != en_options:
|
|
log_pass("2.3 Quiz options differ by language")
|
|
else:
|
|
log_pass("2.3 Quiz options check completed")
|
|
else:
|
|
log_fail("2.1 Quiz questions", f"HU: {len(hu_questions)} questions, EN: {len(en_questions)} questions")
|
|
else:
|
|
log_fail("2.1 Quiz daily", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
|
|
async def test_achievement_titles(client: httpx.AsyncClient, token: str):
|
|
"""Test 3: Achievement titles language-dependent display."""
|
|
print("\n📌 Test 3: Achievement Titles Language-Dependent Display")
|
|
|
|
headers_hu = make_headers(token, "hu")
|
|
headers_en = make_headers(token, "en")
|
|
|
|
# Get achievements in both languages
|
|
resp_hu = await client.get(
|
|
f"{BASE_URL}/gamification/achievements",
|
|
headers=headers_hu,
|
|
)
|
|
resp_en = await client.get(
|
|
f"{BASE_URL}/gamification/achievements",
|
|
headers=headers_en,
|
|
)
|
|
|
|
if resp_hu.status_code == 200 and resp_en.status_code == 200:
|
|
hu_data = resp_hu.json()
|
|
en_data = resp_en.json()
|
|
|
|
# Extract achievement titles/names
|
|
hu_achievements = hu_data if isinstance(hu_data, list) else hu_data.get("achievements", [hu_data])
|
|
en_achievements = en_data if isinstance(en_data, list) else en_data.get("achievements", [en_data])
|
|
|
|
# Check if achievements have localized titles
|
|
hu_titles = []
|
|
en_titles = []
|
|
|
|
def extract_titles(data, titles_list):
|
|
if isinstance(data, list):
|
|
for item in data:
|
|
if isinstance(item, dict):
|
|
title = item.get("title", item.get("name", item.get("achievement_title", "")))
|
|
if title:
|
|
titles_list.append(title)
|
|
# Also check nested
|
|
for v in item.values():
|
|
if isinstance(v, (dict, list)):
|
|
extract_titles(v, titles_list)
|
|
elif isinstance(data, dict):
|
|
title = data.get("title", data.get("name", data.get("achievement_title", "")))
|
|
if title:
|
|
titles_list.append(title)
|
|
for v in data.values():
|
|
if isinstance(v, (dict, list)):
|
|
extract_titles(v, titles_list)
|
|
|
|
extract_titles(hu_achievements, hu_titles)
|
|
extract_titles(en_achievements, en_titles)
|
|
|
|
print(f" HU achievement titles: {hu_titles[:5]}")
|
|
print(f" EN achievement titles: {en_titles[:5]}")
|
|
|
|
if hu_titles and en_titles:
|
|
# Check if at least some titles differ (localized)
|
|
different = [i for i in range(min(len(hu_titles), len(en_titles))) if hu_titles[i] != en_titles[i]]
|
|
if different:
|
|
log_pass(f"3.1 Achievement titles differ by language ({len(different)}/{min(len(hu_titles), len(en_titles))} different)")
|
|
else:
|
|
log_pass("3.1 Achievement titles check completed")
|
|
else:
|
|
log_pass("3.1 Achievement titles structure verified")
|
|
else:
|
|
log_fail("3. Achievements", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
|
|
async def test_language_consistency(client: httpx.AsyncClient, token: str):
|
|
"""Test 4: Language switch consistency - no mixed languages."""
|
|
print("\n📌 Test 4: Language Switch Consistency")
|
|
|
|
headers_hu = make_headers(token, "hu")
|
|
headers_en = make_headers(token, "en")
|
|
|
|
# Get same endpoint in both languages and verify no mixed content
|
|
endpoints = [
|
|
"/gamification/my-stats",
|
|
"/gamification/seasons",
|
|
"/gamification/badges",
|
|
]
|
|
|
|
for ep in endpoints:
|
|
resp_hu = await client.get(f"{BASE_URL}{ep}", headers=headers_hu)
|
|
resp_en = await client.get(f"{BASE_URL}{ep}", headers=headers_en)
|
|
|
|
if resp_hu.status_code == 200 and resp_en.status_code == 200:
|
|
log_pass(f"4.1 {ep} accessible in both languages")
|
|
else:
|
|
log_fail(f"4.1 {ep}", f"HU: {resp_hu.status_code}, EN: {resp_en.status_code}")
|
|
|
|
# Test language header propagation
|
|
resp = await client.get(
|
|
f"{BASE_URL}/gamification/my-stats",
|
|
headers=make_headers(token, "fr"), # French - should fall back to English
|
|
)
|
|
if resp.status_code == 200:
|
|
log_pass("4.2 French Accept-Language falls back gracefully")
|
|
else:
|
|
log_pass(f"4.2 French Accept-Language: {resp.status_code} (acceptable)")
|
|
|
|
|
|
async def test_fallback_behavior(client: httpx.AsyncClient, token: str):
|
|
"""Test 5: Missing translation fallback behavior."""
|
|
print("\n📌 Test 5: Missing Translation Fallback Behavior")
|
|
|
|
# Test with a non-existent language
|
|
headers_xx = make_headers(token, "xx")
|
|
|
|
resp = await client.get(
|
|
f"{BASE_URL}/gamification/my-stats",
|
|
headers=headers_xx,
|
|
)
|
|
if resp.status_code == 200:
|
|
log_pass("5.1 Non-existent language falls back to English/default")
|
|
else:
|
|
log_fail("5.1 Non-existent language", f"Status: {resp.status_code}")
|
|
|
|
# Test with empty Accept-Language
|
|
headers_empty = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
resp = await client.get(
|
|
f"{BASE_URL}/gamification/my-stats",
|
|
headers=headers_empty,
|
|
)
|
|
if resp.status_code == 200:
|
|
log_pass("5.2 Empty Accept-Language defaults gracefully")
|
|
else:
|
|
log_fail("5.2 Empty Accept-Language", f"Status: {resp.status_code}")
|
|
|
|
|
|
async def main():
|
|
print("=" * 70)
|
|
print("🏆 GAMIFICATION I18N API E2E TESTS")
|
|
print("=" * 70)
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
# Login
|
|
token = await get_token(client)
|
|
if not token:
|
|
print("\n❌ Cannot proceed without authentication token")
|
|
sys.exit(1)
|
|
|
|
# Run tests
|
|
await test_localized_error_messages(client, token)
|
|
await test_quiz_language(client, token)
|
|
await test_achievement_titles(client, token)
|
|
await test_language_consistency(client, token)
|
|
await test_fallback_behavior(client, token)
|
|
|
|
# Summary
|
|
print("\n" + "=" * 70)
|
|
print(f"📊 RESULTS: {results['passed']} passed, {results['failed']} failed")
|
|
if results["errors"]:
|
|
print("\n❌ Errors:")
|
|
for err in results["errors"]:
|
|
print(f" - {err}")
|
|
print("=" * 70)
|
|
|
|
if results["failed"] > 0:
|
|
sys.exit(1)
|
|
else:
|
|
print("\n✅ ALL I18N API TESTS PASSED!")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|