146 lines
9.1 KiB
Python
146 lines
9.1 KiB
Python
"""Run with: python -m unittest discover -s tests (from app/). No database needed."""
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from fastapi.testclient import TestClient
|
|
import main
|
|
from i18n import ENGLISH, current_language, gettext, safe_return_path, format_time, format_datetime
|
|
|
|
|
|
class TranslationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.token = current_language.set('de')
|
|
|
|
def tearDown(self):
|
|
current_language.reset(self.token)
|
|
|
|
def test_time_formats_include_midnight_and_noon(self):
|
|
for hour, minute, english, german in (
|
|
(0, 0, '12:00 AM', '00:00'),
|
|
(0, 5, '12:05 AM', '00:05'),
|
|
(9, 7, '9:07 AM', '09:07'),
|
|
(12, 0, '12:00 PM', '12:00'),
|
|
(20, 30, '8:30 PM', '20:30'),
|
|
(23, 59, '11:59 PM', '23:59'),
|
|
):
|
|
value = datetime(2026, 9, 14, hour, minute)
|
|
for language, expected in [('en', english), ('de', german)]:
|
|
with self.subTest(language=language, hour=hour, minute=minute):
|
|
current_language.set(language)
|
|
self.assertEqual(format_time(value), expected)
|
|
self.assertEqual(format_datetime(value), f'14.09.2026 {expected}')
|
|
|
|
def test_event_card_uses_selected_time_format(self):
|
|
start = datetime(2026, 9, 14, 20, 30)
|
|
row = (1, 'Band', start, None, 'Venue', 'Hagen', 'Deutschland', 'concert', None, 'public', False)
|
|
for language, expected in [('en', '8:30 PM'), ('de', '20:30')]:
|
|
current_language.set(language)
|
|
card = main.serialize_concert_card(row)
|
|
self.assertEqual(card['time'], expected)
|
|
self.assertEqual(card['_start_datetime'], start)
|
|
|
|
def test_templates_compile_and_catalog_covers_literal_translations(self):
|
|
for path in Path(main.BASE_DIR, 'templates').glob('*.html'):
|
|
with self.subTest(template=path.name):
|
|
main.templates.get_template(path.name)
|
|
tree = main.templates.parse(path.read_text())
|
|
from jinja2 import nodes
|
|
for node in tree.find_all(nodes.Call):
|
|
if isinstance(node.node, nodes.Name) and node.node.name == '_' and node.args and isinstance(node.args[0], nodes.Const):
|
|
self.assertIn(node.args[0].value, ENGLISH)
|
|
|
|
def test_language_persists_and_can_be_changed_before_login(self):
|
|
with patch.object(main, 'COOKIE_SECURE', False):
|
|
client = TestClient(main.app)
|
|
response = client.get('/login')
|
|
self.assertIn('<html lang="de">', response.text)
|
|
self.assertIn('Anmelden', response.text)
|
|
response = client.get('/language/en?next=/login')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn('<html lang="en">', response.text)
|
|
self.assertIn('Log in', response.text)
|
|
self.assertNotIn('Anmelden', response.text)
|
|
self.assertIn('HttpOnly', response.history[0].headers['set-cookie'])
|
|
self.assertEqual(response.headers['content-language'], 'en')
|
|
self.assertIn('Cookie', response.headers['vary'])
|
|
for path, title in [('/datenschutz', 'Privacy policy'), ('/impressum', 'Legal notice')]:
|
|
page = client.get(path)
|
|
self.assertIn(title, page.text)
|
|
self.assertIn('aria-label="Switch to German"', page.text)
|
|
response = client.get('/language/de?next=/login')
|
|
self.assertIn('Anmelden', response.text)
|
|
self.assertIn('<html lang="de">', response.text)
|
|
|
|
def test_language_validation_and_safe_return(self):
|
|
with patch.object(main, 'COOKIE_SECURE', False):
|
|
client = TestClient(main.app)
|
|
self.assertEqual(client.get('/language/fr').status_code, 400)
|
|
for target in ['https://evil.example', '//evil.example', '/\\evil.example', '/\nevil', '/language/en']:
|
|
self.assertEqual(safe_return_path(target), '/')
|
|
target = '/events/past?q=Iron+Maiden&category=concert'
|
|
response = client.get('/language/en', params={'next': target}, follow_redirects=False)
|
|
self.assertEqual(response.headers['location'], target)
|
|
|
|
def test_translations_are_request_local(self):
|
|
def read(language):
|
|
with patch.object(main, 'COOKIE_SECURE', False):
|
|
client = TestClient(main.app)
|
|
client.cookies.set('metalcircle_language', language)
|
|
return client.get('/login').text
|
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
languages = ['de', 'en'] * 8
|
|
for language, page in zip(languages, pool.map(read, languages)):
|
|
self.assertIn(f'<html lang="{language}">', page)
|
|
self.assertIn('Log in' if language == 'en' else 'Anmelden', page)
|
|
self.assertEqual(gettext('Anmelden'), 'Anmelden')
|
|
|
|
def test_user_content_is_escaped_and_never_translated(self):
|
|
current_language.set('en')
|
|
page = main.templates.get_template('_user_menu.html').render(user={
|
|
'display_name': '<script>Nachrichten</script>', 'is_admin': True, 'notification_count': 2,
|
|
})
|
|
self.assertIn('<script>Nachrichten</script>', page)
|
|
self.assertIn('My profile', page)
|
|
self.assertIn('new message(s) or request(s)', page)
|
|
|
|
def test_every_page_renders_with_populated_content_in_both_languages(self):
|
|
from jinja2 import ChainableUndefined
|
|
import os
|
|
env = main.templates.overlay(undefined=ChainableUndefined)
|
|
member = dict(id=1, username='sample', display_name='Nachrichten <test>', email='sample@example.invalid', is_admin=True, notification_count=2, created_at='14.09.2026')
|
|
event = dict(id=1, concert_id=1, artist='Nachrichten <test>', title='Nachrichten <test>', venue='Venue', city='Hagen', country='Deutschland', date='14.09.2026', time='20:00', event_type='concert', event_type_label='Konzert', visibility='friends', is_past=True, is_verified=True, concert_count=2, name='Venue', rating=4, favorite_song='Nachrichten <test>', notes='Nachrichten <test>', photos=[dict(id=1, path='/photo.jpg')], bands=[])
|
|
badge = dict(code='founder', name='Gründer', description='Von Anfang an dabei und MetalCircle aufgebaut', earned=True, icon='⚔️', image_path='/patch.jpg')
|
|
event['badges'] = [badge]
|
|
event['linked_events'] = []
|
|
profile = dict(member, attended_concerts=1)
|
|
context = dict(user=member, users=[member], managed_user=member, concert=event, profile=profile, filters={}, stats={}, connections=dict(friends=[member], incoming=[member], outgoing=[member], blocked=[member]), badges=[badge], patch_groups=[dict(label='⚔️ Spezial-Patches', patches=[badge])], venues=[event, dict(event, id=2)], entries=[event], upcoming_concerts=[event], past_concerts=[event], events=[event], followed_bands=[dict(name='Nachrichten <test>', key='test')], followed_venues=[event], linkable_events=[event], invitable_users=[member], invited_user_ids=[1], attending_users=[member], maybe_users=[member], ticket_seekers=[member], ticket_offers=[member], attending_count=2, maybe_count=1, ticket_seeker_count=1, ticket_offer_count=1, comments=[dict(member, body='Nachrichten <test>')], is_own_profile=True, can_view_details=True, can_edit_title=True, can_edit_details=True, can_manage_access=True, can_delete=True, can_edit=True, can_manage=True, user_results=[member], friend_requests=[member], event_invitations=[dict(event, invited_by='Nachrichten <test>')], conversations=[dict(member, unread_count=1, last_message='Nachrichten <test>')], messages=[dict(body='Nachrichten <test>')])
|
|
for language in ('de', 'en'):
|
|
current_language.set(language)
|
|
for path in Path(main.BASE_DIR, 'templates').glob('*.html'):
|
|
with self.subTest(language=language, template=path.name):
|
|
from bug_reporter import CATEGORIES, SEVERITIES
|
|
page = env.get_template(path.name).render(**context, categories=CATEGORIES, severities=SEVERITIES, form={})
|
|
if not path.name.startswith('_'):
|
|
self.assertIn(f'<html lang="{language}">', page)
|
|
self.assertIn('class="language-switch"', page)
|
|
if os.environ.get('I18N_RENDER_DIR'):
|
|
target = Path(os.environ['I18N_RENDER_DIR'], language)
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
(target / path.name).write_text(page)
|
|
|
|
def test_server_validation_is_localized(self):
|
|
current_language.set('en')
|
|
with self.assertRaisesRegex(ValueError, 'Instagram') as error:
|
|
main.normalize_instagram_url('https://evil.example/test')
|
|
self.assertIn('Please enter', str(error.exception))
|
|
from starlette.requests import Request
|
|
response = main.change_language(Request({'type': 'http', 'headers': []}), 'invalid')
|
|
self.assertEqual(response.body, b'Invalid language.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|