from pathlib import Path from datetime import date from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import Client, TestCase, override_settings from django.urls import reverse from django.utils import timezone from workflows.models import OffboardingRequest, OnboardingRequest, ScheduledWelcomeEmail, UserNotification, UserProfile from workflows.roles import ROLE_PLATFORM_OWNER, assign_user_role from workflows.tasks import process_offboarding_request, process_onboarding_request, send_scheduled_welcome_email @override_settings(PDF_OUTPUT_DIR=Path('/tmp/onoff_test_pdfs')) class NotificationFlowTests(TestCase): def setUp(self): user_model = get_user_model() self.requester = user_model.objects.create_user( username='notify_user', password='secret123', email='requester@workdock.de', first_name='Nina', last_name='Requester', ) @patch('workflows.tasks._apply_notification_rules') @patch('workflows.tasks._schedule_welcome_email') @patch('workflows.tasks.upload_to_nextcloud') @patch('workflows.tasks._send_templated_email') @patch('workflows.tasks._generate_onboarding_pdf') def test_onboarding_success_creates_success_notification( self, mock_generate_pdf, mock_send_templated_email, mock_upload, mock_schedule, mock_rules, ): pdf_path = Path('/tmp/onoff_test_pdfs/onboarding_letter_Nina_Notify.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 request_obj = OnboardingRequest.objects.create( full_name='Nina Notify', gender='frau', job_title='Engineer', department='IT', work_email='nina.notify@workdock.de', contract_start=date(2026, 11, 1), employment_type='unbefristet', onboarded_by_email=self.requester.email, onboarded_by_name='Nina Requester', agreement='accepted', ) process_onboarding_request(request_obj.id) notification = UserNotification.objects.get(user=self.requester) self.assertEqual(notification.level, UserNotification.LEVEL_SUCCESS) self.assertIn('Onboarding abgeschlossen', notification.title) self.assertEqual(notification.link_url, '/requests/') mock_upload.assert_called_once_with(pdf_path, pdf_path.name) @patch('workflows.tasks._generate_onboarding_pdf', side_effect=RuntimeError('PDF kaputt')) def test_onboarding_failure_creates_error_notification(self, mock_generate_pdf): request_obj = OnboardingRequest.objects.create( full_name='Lara Broken', gender='frau', job_title='Engineer', department='IT', work_email='lara.broken@workdock.de', contract_start=date(2026, 11, 1), employment_type='unbefristet', onboarded_by_email=self.requester.email, onboarded_by_name='Nina Requester', agreement='accepted', ) with self.assertRaises(RuntimeError): process_onboarding_request(request_obj.id) notification = UserNotification.objects.get(user=self.requester) self.assertEqual(notification.level, UserNotification.LEVEL_ERROR) self.assertIn('Onboarding fehlgeschlagen', notification.title) self.assertIn('PDF kaputt', notification.body) @patch('workflows.tasks._apply_notification_rules') @patch('workflows.tasks.upload_to_nextcloud') @patch('workflows.tasks._send_templated_email') @patch('workflows.tasks._generate_offboarding_pdf') def test_offboarding_success_creates_success_notification( self, mock_generate_pdf, mock_send_templated_email, mock_upload, mock_rules, ): pdf_path = Path('/tmp/onoff_test_pdfs/offboarding_letter_Nina_Notify.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 request_obj = OffboardingRequest.objects.create( full_name='Nina Notify', work_email='nina.notify@workdock.de', department='IT', job_title='Engineer', last_working_day=date(2026, 12, 31), requested_by_email=self.requester.email, requested_by_name='Nina Requester', ) process_offboarding_request(request_obj.id) notification = UserNotification.objects.get(user=self.requester) self.assertEqual(notification.level, UserNotification.LEVEL_SUCCESS) self.assertIn('Offboarding abgeschlossen', notification.title) self.assertEqual(notification.link_url, '/requests/') mock_upload.assert_called_once_with(pdf_path, pdf_path.name) @patch('workflows.tasks._apply_notification_rules') @patch('workflows.tasks._schedule_welcome_email') @patch('workflows.tasks.upload_to_nextcloud') @patch('workflows.tasks._send_templated_email') @patch('workflows.tasks._generate_onboarding_pdf') def test_onboarding_success_notification_respects_user_preferences( self, mock_generate_pdf, mock_send_templated_email, mock_upload, mock_schedule, mock_rules, ): profile, _ = UserProfile.objects.get_or_create(user=self.requester) profile.notification_preferences = { 'onboarding_success': False, 'onboarding_failure': True, 'offboarding_success': True, 'offboarding_failure': True, } profile.save(update_fields=['notification_preferences', 'updated_at']) pdf_path = Path('/tmp/onoff_test_pdfs/onboarding_letter_Pref_Off.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 request_obj = OnboardingRequest.objects.create( full_name='Pref Off', gender='frau', job_title='Engineer', department='IT', work_email='pref.off@workdock.de', contract_start=date(2026, 11, 1), employment_type='unbefristet', onboarded_by_email=self.requester.email, onboarded_by_name='Nina Requester', agreement='accepted', ) process_onboarding_request(request_obj.id) self.assertFalse(UserNotification.objects.filter(user=self.requester).exists()) class NotificationHeaderTests(TestCase): def setUp(self): user_model = get_user_model() self.user = user_model.objects.create_user( username='notify_header', password='secret123', email='notify.header@workdock.de', ) self.client.force_login(self.user) def test_mark_notification_read_marks_single_entry(self): notification = UserNotification.objects.create( user=self.user, title='Backup fehlgeschlagen', body='Bitte prüfen.', level=UserNotification.LEVEL_ERROR, link_url='/requests/', ) response = self.client.post( reverse('mark_notification_read', args=[notification.id]), {'next': '/'}, HTTP_HOST='localhost', ) notification.refresh_from_db() self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], '/') self.assertIsNotNone(notification.read_at) def test_mark_all_notifications_read_marks_unread_items(self): first = UserNotification.objects.create(user=self.user, title='Erfolg', level=UserNotification.LEVEL_SUCCESS) second = UserNotification.objects.create(user=self.user, title='Fehler', level=UserNotification.LEVEL_ERROR) response = self.client.post( reverse('mark_all_notifications_read'), {'next': '/requests/'}, HTTP_HOST='localhost', ) first.refresh_from_db() second.refresh_from_db() self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], '/requests/') self.assertIsNotNone(first.read_at) self.assertIsNotNone(second.read_at) class OperationalNotificationTests(TestCase): def setUp(self): user_model = get_user_model() self.user = user_model.objects.create_user( username='ops_notify', password='secret123', email='ops.notify@workdock.de', ) assign_user_role(self.user, ROLE_PLATFORM_OWNER) self.client = Client(HTTP_HOST='localhost') self.client.force_login(self.user) @patch('workflows.views.create_backup_bundle') def test_backup_success_creates_notification(self, mock_create_backup_bundle): mock_create_backup_bundle.return_value = {'name': 'backup_20260327_101010', 'path': '/tmp/backup'} response = self.client.post(reverse('create_backup_from_admin')) self.assertEqual(response.status_code, 302) notification = UserNotification.objects.get(user=self.user) self.assertEqual(notification.level, UserNotification.LEVEL_SUCCESS) self.assertIn('Backup erstellt', notification.title) def test_backup_success_respects_preferences(self): profile = UserProfile.objects.get(user=self.user) profile.notification_preferences = { 'onboarding_success': True, 'onboarding_failure': True, 'offboarding_success': True, 'offboarding_failure': True, 'backup_success': False, 'backup_failure': True, 'trial_alerts': True, 'system_alerts': True, } profile.save(update_fields=['notification_preferences', 'updated_at']) with patch('workflows.views.create_backup_bundle', return_value={'name': 'backup_20260327_111111', 'path': '/tmp/backup'}): response = self.client.post(reverse('create_backup_from_admin')) self.assertEqual(response.status_code, 302) self.assertFalse(UserNotification.objects.filter(user=self.user).exists()) def test_trial_warning_creates_notification(self): response = self.client.post( reverse('save_portal_trial_config'), { 'is_trial_mode': 'on', 'trial_started_at': '2026-03-27T10:00', 'trial_expires_at': '2026-03-30T10:00', 'restrict_production_integrations': 'on', 'auto_cleanup_enabled': 'on', 'trial_banner_text': 'Trial läuft', 'trial_banner_text_en': 'Trial running', }, ) self.assertEqual(response.status_code, 200) notification = UserNotification.objects.get(user=self.user) self.assertEqual(notification.level, UserNotification.LEVEL_WARNING) self.assertIn('Trial läuft bald ab', notification.title) @override_settings(PDF_OUTPUT_DIR=Path('/tmp/onoff_test_pdfs')) class WelcomeEmailNotificationTests(TestCase): def setUp(self): user_model = get_user_model() self.requester = user_model.objects.create_user( username='welcome_notify_user', password='secret123', email='welcome.requester@workdock.de', ) self.onboarding = OnboardingRequest.objects.create( full_name='Welcome Notify', gender='frau', job_title='Engineer', department='IT', work_email='welcome.notify@workdock.de', contract_start=date(2026, 11, 1), employment_type='unbefristet', onboarded_by_email=self.requester.email, onboarded_by_name='Welcome Requester', agreement='accepted', ) @patch('workflows.tasks._send_templated_email') def test_welcome_email_success_creates_notification(self, mock_send_templated_email): scheduled = ScheduledWelcomeEmail.objects.create( onboarding_request=self.onboarding, recipient_email='welcome.notify@workdock.de', send_at=timezone.now() - timezone.timedelta(minutes=1), status='scheduled', ) send_scheduled_welcome_email(scheduled.id, True) notification = UserNotification.objects.get(user=self.requester) self.assertEqual(notification.level, UserNotification.LEVEL_SUCCESS) self.assertIn('Welcome E-Mail gesendet', notification.title) @patch('workflows.tasks._send_templated_email', side_effect=RuntimeError('SMTP broken')) def test_welcome_email_failure_creates_notification(self, mock_send_templated_email): scheduled = ScheduledWelcomeEmail.objects.create( onboarding_request=self.onboarding, recipient_email='welcome.notify@workdock.de', send_at=timezone.now() - timezone.timedelta(minutes=1), status='scheduled', ) with self.assertRaises(RuntimeError): send_scheduled_welcome_email(scheduled.id, True) notification = UserNotification.objects.get(user=self.requester) self.assertEqual(notification.level, UserNotification.LEVEL_ERROR) self.assertIn('Welcome E-Mail fehlgeschlagen', notification.title) @patch('workflows.tasks._send_templated_email') def test_welcome_email_success_respects_preferences(self, mock_send_templated_email): profile, _ = UserProfile.objects.get_or_create(user=self.requester) profile.notification_preferences = { 'onboarding_success': True, 'onboarding_failure': True, 'offboarding_success': True, 'offboarding_failure': True, 'backup_success': True, 'backup_failure': True, 'welcome_email_success': False, 'welcome_email_failure': True, 'trial_alerts': True, 'system_alerts': True, } profile.save(update_fields=['notification_preferences', 'updated_at']) scheduled = ScheduledWelcomeEmail.objects.create( onboarding_request=self.onboarding, recipient_email='welcome.notify@workdock.de', send_at=timezone.now() - timezone.timedelta(minutes=1), status='scheduled', ) send_scheduled_welcome_email(scheduled.id, True) self.assertFalse(UserNotification.objects.filter(user=self.requester).exists())