snapshot: preserve bilingual smoke test coverage
This commit is contained in:
@@ -57,6 +57,7 @@ Notes:
|
|||||||
- live introduction protocol PDF
|
- live introduction protocol PDF
|
||||||
- Remaining bilingual gap is mostly long-form handbook/wiki copy and a few secondary admin/help texts.
|
- Remaining bilingual gap is mostly long-form handbook/wiki copy and a few secondary admin/help texts.
|
||||||
- CI now validates that translation catalogs compile successfully on push and pull request.
|
- CI now validates that translation catalogs compile successfully on push and pull request.
|
||||||
|
- Automated bilingual smoke coverage now verifies DE/EN request language capture plus English email-template selection for onboarding and welcome flows.
|
||||||
- Dependency stability hardening pins `chardet==5.2.0` so `requests` runs without compatibility warnings in the Docker stack.
|
- Dependency stability hardening pins `chardet==5.2.0` so `requests` runs without compatibility warnings in the Docker stack.
|
||||||
|
|
||||||
## Current implemented scope
|
## Current implemented scope
|
||||||
|
|||||||
@@ -1235,11 +1235,13 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
|||||||
context=email_context,
|
context=email_context,
|
||||||
to=[it_email],
|
to=[it_email],
|
||||||
attachments=[pdf_path],
|
attachments=[pdf_path],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
_send_templated_email(
|
_send_templated_email(
|
||||||
template_key='onboarding_general_info',
|
template_key='onboarding_general_info',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[general_info_email],
|
to=[general_info_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
if request_obj.order_business_cards:
|
if request_obj.order_business_cards:
|
||||||
@@ -1247,6 +1249,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
|||||||
template_key='onboarding_business_card',
|
template_key='onboarding_business_card',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[business_card_email],
|
to=[business_card_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'HR Works' in request_obj.needed_accesses:
|
if 'HR Works' in request_obj.needed_accesses:
|
||||||
@@ -1254,6 +1257,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
|||||||
template_key='onboarding_hr_works',
|
template_key='onboarding_hr_works',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[hr_works_email],
|
to=[hr_works_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'Schlüssel' in request_obj.needed_devices:
|
if 'Schlüssel' in request_obj.needed_devices:
|
||||||
@@ -1261,6 +1265,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
|||||||
template_key='onboarding_key',
|
template_key='onboarding_key',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[key_email],
|
to=[key_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
if request_obj.onboarded_by_email:
|
if request_obj.onboarded_by_email:
|
||||||
@@ -1269,6 +1274,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
|||||||
context=email_context,
|
context=email_context,
|
||||||
to=[request_obj.onboarded_by_email],
|
to=[request_obj.onboarded_by_email],
|
||||||
attachments=[pdf_path],
|
attachments=[pdf_path],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
_apply_notification_rules(
|
_apply_notification_rules(
|
||||||
@@ -1305,11 +1311,13 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
|
|||||||
context=email_context,
|
context=email_context,
|
||||||
to=[it_email],
|
to=[it_email],
|
||||||
attachments=[pdf_path],
|
attachments=[pdf_path],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
_send_templated_email(
|
_send_templated_email(
|
||||||
template_key='offboarding_general_info',
|
template_key='offboarding_general_info',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[general_info_email],
|
to=[general_info_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
had_hr_works = OnboardingRequest.objects.filter(
|
had_hr_works = OnboardingRequest.objects.filter(
|
||||||
@@ -1321,6 +1329,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
|
|||||||
template_key='offboarding_hr_works_disable',
|
template_key='offboarding_hr_works_disable',
|
||||||
context=email_context,
|
context=email_context,
|
||||||
to=[hr_works_email],
|
to=[hr_works_email],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
_send_templated_email(
|
_send_templated_email(
|
||||||
@@ -1328,6 +1337,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
|
|||||||
context=email_context,
|
context=email_context,
|
||||||
to=[request_obj.requested_by_email],
|
to=[request_obj.requested_by_email],
|
||||||
attachments=[pdf_path],
|
attachments=[pdf_path],
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
_apply_notification_rules(
|
_apply_notification_rules(
|
||||||
@@ -1389,6 +1399,7 @@ def send_scheduled_welcome_email(scheduled_email_id: int, force_now: bool = Fals
|
|||||||
to=[scheduled.recipient_email],
|
to=[scheduled.recipient_email],
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
from_email=from_email or None,
|
from_email=from_email or None,
|
||||||
|
language_code=request_obj.preferred_language,
|
||||||
)
|
)
|
||||||
scheduled.status = 'sent'
|
scheduled.status = 'sent'
|
||||||
scheduled.sent_at = timezone.now()
|
scheduled.sent_at = timezone.now()
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
|
|||||||
<li>Use targeted shell checks for render validation when changing templates or routes.</li>
|
<li>Use targeted shell checks for render validation when changing templates or routes.</li>
|
||||||
<li>Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.</li>
|
<li>Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.</li>
|
||||||
<li>Use the dedicated Release Checklist page as the final go/no-go runbook before shipping changes.</li>
|
<li>Use the dedicated Release Checklist page as the final go/no-go runbook before shipping changes.</li>
|
||||||
|
<li>The automated bilingual smoke tests now cover DE/EN request language capture and English email-template rendering.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 id="deploy">12) Deployment and Release Checklist</h2>
|
<h2 id="deploy">12) Deployment and Release Checklist</h2>
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
<li><strong>PDF phase added:</strong> fixed PDF headings, labels, notes, and confirmation text now render in German or English based on the request language, with German as the fallback.</li>
|
<li><strong>PDF phase added:</strong> fixed PDF headings, labels, notes, and confirmation text now render in German or English based on the request language, with German as the fallback.</li>
|
||||||
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
|
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
|
||||||
<li><strong>Not fully bilingual yet:</strong> the main remaining gaps are long-form handbook/wiki copy and a few secondary admin/help texts.</li>
|
<li><strong>Not fully bilingual yet:</strong> the main remaining gaps are long-form handbook/wiki copy and a few secondary admin/help texts.</li>
|
||||||
|
<li><strong>Smoke coverage:</strong> automated tests now verify DE/EN request language capture and English email-template selection for onboarding and welcome email flows.</li>
|
||||||
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>
|
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
193
backend/workflows/tests/test_bilingual_smoke.py
Normal file
193
backend/workflows/tests/test_bilingual_smoke.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from workflows.models import EmployeeProfile, NotificationTemplate, OffboardingRequest, OnboardingRequest, ScheduledWelcomeEmail
|
||||||
|
from workflows.tasks import process_onboarding_request, send_scheduled_welcome_email
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(PDF_OUTPUT_DIR=Path('/tmp/onoff_test_pdfs'))
|
||||||
|
class BilingualSmokeTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
user_model = get_user_model()
|
||||||
|
self.user = user_model.objects.create_user(
|
||||||
|
username='bilingual_user',
|
||||||
|
password='secret123',
|
||||||
|
email='requester@tub.co',
|
||||||
|
first_name='Mia',
|
||||||
|
last_name='Beispiel',
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.profile = EmployeeProfile.objects.create(
|
||||||
|
full_name='Lara Beispiel',
|
||||||
|
first_name='Lara',
|
||||||
|
last_name='Beispiel',
|
||||||
|
department='IT-Service',
|
||||||
|
job_title='Engineer',
|
||||||
|
work_email='lara.beispiel@tub.co',
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch('workflows.views.process_onboarding_request.delay')
|
||||||
|
def test_onboarding_submit_persists_english_language(self, mock_delay):
|
||||||
|
payload = {
|
||||||
|
'first_name': 'Max',
|
||||||
|
'last_name': 'Mustermann',
|
||||||
|
'gender': 'herr',
|
||||||
|
'job_title': 'Consultant',
|
||||||
|
'department': 'IT-Service',
|
||||||
|
'work_email': 'max.mustermann@tub.co',
|
||||||
|
'contract_start': '2026-11-01',
|
||||||
|
'employment_type': 'unbefristet',
|
||||||
|
'group_mailboxes_required_choice': 'nein',
|
||||||
|
'additional_hardware_needed_choice': 'nein',
|
||||||
|
'additional_software_needed_choice': 'nein',
|
||||||
|
'additional_access_needed_choice': 'nein',
|
||||||
|
'successor_required_choice': 'nein',
|
||||||
|
'inherit_phone_number_choice': 'nein',
|
||||||
|
'agreement_confirm': 'on',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost', HTTP_ACCEPT_LANGUAGE='en')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
obj = OnboardingRequest.objects.get(work_email='max.mustermann@tub.co')
|
||||||
|
self.assertEqual(obj.preferred_language, 'en')
|
||||||
|
mock_delay.assert_called_once_with(obj.id)
|
||||||
|
|
||||||
|
@patch('workflows.views.process_onboarding_request.delay')
|
||||||
|
def test_onboarding_submit_persists_german_language(self, mock_delay):
|
||||||
|
payload = {
|
||||||
|
'first_name': 'Erika',
|
||||||
|
'last_name': 'Muster',
|
||||||
|
'gender': 'frau',
|
||||||
|
'job_title': 'Consultant',
|
||||||
|
'department': 'IT-Service',
|
||||||
|
'work_email': 'erika.muster@tub.co',
|
||||||
|
'contract_start': '2026-11-02',
|
||||||
|
'employment_type': 'unbefristet',
|
||||||
|
'group_mailboxes_required_choice': 'nein',
|
||||||
|
'additional_hardware_needed_choice': 'nein',
|
||||||
|
'additional_software_needed_choice': 'nein',
|
||||||
|
'additional_access_needed_choice': 'nein',
|
||||||
|
'successor_required_choice': 'nein',
|
||||||
|
'inherit_phone_number_choice': 'nein',
|
||||||
|
'agreement_confirm': 'on',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost', HTTP_ACCEPT_LANGUAGE='de')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
obj = OnboardingRequest.objects.get(work_email='erika.muster@tub.co')
|
||||||
|
self.assertEqual(obj.preferred_language, 'de')
|
||||||
|
mock_delay.assert_called_once_with(obj.id)
|
||||||
|
|
||||||
|
@patch('workflows.views.process_offboarding_request.delay')
|
||||||
|
def test_offboarding_submit_persists_english_language(self, mock_delay):
|
||||||
|
payload = {
|
||||||
|
'full_name': self.profile.full_name,
|
||||||
|
'work_email': self.profile.work_email,
|
||||||
|
'department': self.profile.department,
|
||||||
|
'job_title': self.profile.job_title,
|
||||||
|
'last_working_day': '2026-12-31',
|
||||||
|
'notes': 'Disable accounts.',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f'/offboarding/new/?profile={self.profile.id}',
|
||||||
|
payload,
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
HTTP_ACCEPT_LANGUAGE='en',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
obj = OffboardingRequest.objects.get(work_email=self.profile.work_email)
|
||||||
|
self.assertEqual(obj.preferred_language, 'en')
|
||||||
|
mock_delay.assert_called_once_with(obj.id)
|
||||||
|
|
||||||
|
@patch('workflows.tasks._apply_notification_rules')
|
||||||
|
@patch('workflows.tasks._schedule_welcome_email')
|
||||||
|
@patch('workflows.tasks.upload_to_nextcloud')
|
||||||
|
@patch('workflows.tasks._send_workflow_email')
|
||||||
|
@patch('workflows.tasks._generate_onboarding_pdf')
|
||||||
|
def test_onboarding_task_uses_english_template_for_english_request(
|
||||||
|
self,
|
||||||
|
mock_generate_pdf,
|
||||||
|
mock_send_workflow_email,
|
||||||
|
mock_upload,
|
||||||
|
mock_schedule,
|
||||||
|
mock_rules,
|
||||||
|
):
|
||||||
|
pdf_path = Path('/tmp/onoff_test_pdfs/onboarding_letter_English_Person.pdf')
|
||||||
|
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
pdf_path.write_bytes(b'%PDF-1.4\n%test\n')
|
||||||
|
mock_generate_pdf.return_value = pdf_path
|
||||||
|
NotificationTemplate.objects.update_or_create(
|
||||||
|
key='onboarding_it',
|
||||||
|
defaults={
|
||||||
|
'subject_template': 'DE IT',
|
||||||
|
'subject_template_en': 'EN IT',
|
||||||
|
'body_template': 'DE body',
|
||||||
|
'body_template_en': 'EN body',
|
||||||
|
'is_active': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_obj = OnboardingRequest.objects.create(
|
||||||
|
full_name='English Person',
|
||||||
|
gender='herr',
|
||||||
|
job_title='Engineer',
|
||||||
|
department='IT-Service',
|
||||||
|
work_email='english.person@tub.co',
|
||||||
|
contract_start=date(2026, 11, 1),
|
||||||
|
employment_type='unbefristet',
|
||||||
|
onboarded_by_email='requester@tub.co',
|
||||||
|
onboarded_by_name='Mia Beispiel',
|
||||||
|
agreement='accepted',
|
||||||
|
preferred_language='en',
|
||||||
|
)
|
||||||
|
|
||||||
|
process_onboarding_request(request_obj.id)
|
||||||
|
|
||||||
|
first_call = mock_send_workflow_email.call_args_list[0].kwargs
|
||||||
|
self.assertEqual(first_call['subject'], 'EN IT')
|
||||||
|
self.assertEqual(first_call['body'], 'EN body')
|
||||||
|
|
||||||
|
@patch('workflows.tasks._send_workflow_email')
|
||||||
|
def test_welcome_email_uses_english_template_for_english_request(self, mock_send_workflow_email):
|
||||||
|
NotificationTemplate.objects.update_or_create(
|
||||||
|
key='onboarding_welcome',
|
||||||
|
defaults={
|
||||||
|
'subject_template': 'DE Welcome',
|
||||||
|
'subject_template_en': 'EN Welcome',
|
||||||
|
'body_template': 'DE Welcome Body',
|
||||||
|
'body_template_en': 'EN Welcome Body',
|
||||||
|
'is_active': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
onboarding = OnboardingRequest.objects.create(
|
||||||
|
full_name='Welcome Person',
|
||||||
|
gender='frau',
|
||||||
|
job_title='Manager',
|
||||||
|
department='IT-Service',
|
||||||
|
work_email='welcome.person@tub.co',
|
||||||
|
contract_start=date(2026, 11, 1),
|
||||||
|
employment_type='unbefristet',
|
||||||
|
onboarded_by_email='requester@tub.co',
|
||||||
|
agreement='accepted',
|
||||||
|
preferred_language='en',
|
||||||
|
)
|
||||||
|
scheduled = ScheduledWelcomeEmail.objects.create(
|
||||||
|
onboarding_request=onboarding,
|
||||||
|
recipient_email='welcome.person@tub.co',
|
||||||
|
send_at=timezone.now(),
|
||||||
|
status='scheduled',
|
||||||
|
)
|
||||||
|
|
||||||
|
send_scheduled_welcome_email(scheduled.id, True)
|
||||||
|
|
||||||
|
kwargs = mock_send_workflow_email.call_args.kwargs
|
||||||
|
self.assertEqual(kwargs['subject'], 'EN Welcome')
|
||||||
|
self.assertEqual(kwargs['body'], 'EN Welcome Body')
|
||||||
@@ -399,7 +399,7 @@ def onboarding_create(request):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
obj = form.save()
|
obj = form.save()
|
||||||
obj.onboarded_by_name = _display_user_name(request.user)
|
obj.onboarded_by_name = _display_user_name(request.user)
|
||||||
obj.preferred_language = (get_language() or 'de').split('-')[0]
|
obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0])
|
||||||
obj.save(update_fields=['onboarded_by_name', 'preferred_language'])
|
obj.save(update_fields=['onboarded_by_name', 'preferred_language'])
|
||||||
process_onboarding_request.delay(obj.id)
|
process_onboarding_request.delay(obj.id)
|
||||||
return redirect(f"/onboarding/new/?saved=1&id={obj.id}")
|
return redirect(f"/onboarding/new/?saved=1&id={obj.id}")
|
||||||
@@ -572,7 +572,7 @@ def offboarding_create(request):
|
|||||||
else:
|
else:
|
||||||
obj.requested_by_email = settings.DEFAULT_FROM_EMAIL
|
obj.requested_by_email = settings.DEFAULT_FROM_EMAIL
|
||||||
obj.requested_by_name = _display_user_name(request.user)
|
obj.requested_by_name = _display_user_name(request.user)
|
||||||
obj.preferred_language = (get_language() or 'de').split('-')[0]
|
obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0])
|
||||||
obj.save()
|
obj.save()
|
||||||
process_offboarding_request.delay(obj.id)
|
process_offboarding_request.delay(obj.id)
|
||||||
return redirect(f"/offboarding/new/?saved=1&id={obj.id}")
|
return redirect(f"/offboarding/new/?saved=1&id={obj.id}")
|
||||||
|
|||||||
Reference in New Issue
Block a user