diff --git a/.env.example b/.env.example index daaada4..97b00b4 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,7 @@ MAIL_DEFAULT_SENDER=noreply@example.com # Company Settings COMPANY_NAME=Ihr Firmenname + +# HR Notification Settings +HR_EMAIL=hr@example.com +APPLICATION_URL_BASE=http://localhost:5000 diff --git a/app.py b/app.py index 4a7966f..6feda0c 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ import yaml import re from datetime import datetime, timedelta from pathlib import Path -from flask import Flask, render_template, request, redirect, url_for, flash, session +from flask import Flask, render_template, request, redirect, url_for, flash, session, send_from_directory, abort from flask_mail import Mail, Message from flask_wtf.csrf import CSRFProtect, CSRFError from werkzeug.utils import secure_filename @@ -120,6 +120,57 @@ Mit freundlichen Grüßen return False +def send_hr_notification(session_id, app_data): + """Send HR notification email when application is submitted""" + if not app.config.get('HR_EMAIL'): + app.logger.warning("HR_EMAIL not configured, skipping HR notification") + return False + + # Build application URL + application_url = f"{app.config['APPLICATION_URL_BASE']}/application/{session_id}/" + + # Get personal info + personal_info = app_data.get('personal_info', {}) + firstname = personal_info.get('firstname', 'N/A') + name = personal_info.get('name', 'N/A') + email = app_data.get('email', 'N/A') + job_name = app_data.get('job_name', 'N/A') + file_count = len(app_data.get('uploaded_files', [])) + + # Format timestamp + submitted_at = app_data.get('submitted_at', datetime.now().isoformat()) + try: + timestamp = datetime.fromisoformat(submitted_at).strftime('%d.%m.%Y %H:%M') + except (ValueError, TypeError): + timestamp = submitted_at + + subject = f"Neue Bewerbung eingegangen: {job_name}" + body = f"""Sehr geehrtes HR-Team, + +es ist eine neue Bewerbung eingegangen: + +Position: {job_name} +Name: {firstname} {name} +E-Mail: {email} +Eingereicht am: {timestamp} +Anzahl der hochgeladenen Dokumente: {file_count} + +Sie können die vollständige Bewerbung hier einsehen: +{application_url} + +Mit freundlichen Grüßen, +Ihr Bewerbungssystem +""" + + try: + msg = Message(subject=subject, recipients=[app.config['HR_EMAIL']], body=body) + mail.send(msg) + return True + except Exception as e: + app.logger.error(f"Failed to send HR notification email: {e}") + return False + + def validate_email(email): """Validate email format""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' @@ -522,6 +573,9 @@ def submit_application(session_id): save_application_data(session_id, app_data) + # Send HR notification + send_hr_notification(session_id, app_data) + return redirect(url_for('page5_confirmation', session_id=session_id)) @@ -539,6 +593,47 @@ def page5_confirmation(session_id): email=app_data['email']) +@app.route('/application//') +def view_application(session_id): + """View complete application details (for HR)""" + app_data = load_application_data(session_id) + if not app_data: + abort(404) + + return render_template('application_view.html', + session_id=session_id, + app_data=app_data) + + +@app.route('/application//download/') +def download_file(session_id, filename): + """Download an uploaded file""" + app_data = load_application_data(session_id) + if not app_data: + abort(404) + + # Verify the file is in the uploaded files list + uploaded_files = app_data.get('uploaded_files', []) + file_found = False + + for file_info in uploaded_files: + if file_info.get('stored_name') == filename: + file_found = True + break + + if not file_found: + abort(404) + + # Serve the file + attachments_path = get_attachments_path(session_id) + file_path = os.path.join(attachments_path, filename) + + if not os.path.exists(file_path): + abort(404) + + return send_from_directory(attachments_path, filename, as_attachment=True) + + @app.route('/resume/') def resume_application(session_id): """Resume application from email link""" diff --git a/config.py b/config.py index 4727547..a830778 100644 --- a/config.py +++ b/config.py @@ -21,6 +21,10 @@ class Config: # Company name for emails COMPANY_NAME = os.getenv('COMPANY_NAME', 'Unser Unternehmen') + # HR notification settings + HR_EMAIL = os.getenv('HR_EMAIL') + APPLICATION_URL_BASE = os.getenv('APPLICATION_URL_BASE', 'http://localhost:5000') + # Application settings APPLICATIONS_FOLDER = './applications' MAX_FILE_SIZE = 4 * 1024 * 1024 # 4 MB in bytes diff --git a/prompts/004-hr-notification-review-page.md b/prompts/004-hr-notification-review-page.md new file mode 100644 index 0000000..88ecab3 --- /dev/null +++ b/prompts/004-hr-notification-review-page.md @@ -0,0 +1,137 @@ +# Prompt 004: HR Email Notifications and Application Review Page + +## Objective +Implement HR email notifications when an application is submitted and create a review page where HR can view complete application details and download attachments. + +## Requirements + +### 1. HR Email Notification +- When user submits application on page 4 (submit_application route), send email to HR +- HR email address configurable via environment variable `HR_EMAIL` +- Email should be in German +- Email should include: + - Job position name + - Applicant's name and email + - Submission timestamp + - Direct link to application review page: `http://yourdomain.com/application//` + - Brief summary (e.g., number of documents uploaded) + +### 2. Application Review Page Route +- Create new route: `/application//` +- No additional authentication needed (session ID provides security through obscurity) +- Display all application information in a clean, readable format +- Show all uploaded files with download links + +### 3. Application Review Template +- Create `templates/application_view.html` +- Display in German: + - Job position + - Personal information (all fields from page 2) + - Motivation answers (all fields from page 3) + - List of uploaded documents with: + - Original filename + - File size + - Download link for each file + - Submission timestamp + - Email address used for application + +### 4. File Download Route +- Create route: `/application//download/` +- Serve the actual uploaded file +- Use `send_from_directory` with proper security checks +- Verify filename exists in application's uploaded_files list + +### 5. Configuration Updates +- Add `HR_EMAIL` to `config.py` with environment variable +- Add `HR_EMAIL` to `.env.example` +- Add `APPLICATION_URL_BASE` to config for constructing full URLs in emails + +### 6. Error Handling +- Handle missing application gracefully (404 error) +- Handle missing files gracefully (404 error) +- Validate session_id format + +### 7. Testing Requirements +- Test HR email is sent on application submission +- Test email contains correct information and link +- Test application review page displays all data correctly +- Test file download works +- Test error cases (invalid session_id, missing files) +- Test that HR email is not sent if email sending fails + +## Technical Implementation Notes + +### Email Template (German) +``` +Betreff: Neue Bewerbung eingegangen: {job_name} + +Sehr geehrtes HR-Team, + +es ist eine neue Bewerbung eingegangen: + +Position: {job_name} +Name: {firstname} {name} +E-Mail: {email} +Eingereicht am: {timestamp} +Anzahl der hochgeladenen Dokumente: {file_count} + +Sie können die vollständige Bewerbung hier einsehen: +{application_url} + +Mit freundlichen Grüßen, +Ihr Bewerbungssystem +``` + +### File Storage Structure +Files are already stored in: `applications//files/` +The stored_filename is in the uploaded_files list in the YAML data. + +### Security Considerations +- Session IDs are UUIDs (sufficiently random and unguessable) +- Only allow downloading files that are listed in the application's uploaded_files +- Use `secure_filename()` when handling file operations +- No directory traversal attacks (verify file is in correct session folder) + +## Files to Modify + +1. **config.py** + - Add HR_EMAIL configuration + - Add APPLICATION_URL_BASE configuration + +2. **.env.example** + - Add HR_EMAIL example + - Add APPLICATION_URL_BASE example + +3. **app.py** + - Modify `submit_application` route to send HR email + - Add new route: `view_application(session_id)` + - Add new route: `download_file(session_id, filename)` + - Add email helper function for HR notification + +4. **templates/application_view.html** (NEW) + - Create clean layout for HR to review application + - Display all fields in German + - Provide download links for attachments + +## Files to Create (Tests) + +1. **tests/test_hr_notifications.py** (NEW) + - Test HR email is sent on submission + - Test email content is correct + - Test email contains proper link + +2. **tests/test_application_view.py** (NEW) + - Test application view page renders correctly + - Test all data is displayed + - Test file download works + - Test 404 for invalid session_id + - Test 404 for invalid filename + +## Success Criteria +- HR receives email when application is submitted +- Email contains all required information in German +- Application review page displays all application data +- HR can download all uploaded files +- All existing tests still pass +- New tests achieve high coverage for new functionality +- No security vulnerabilities introduced diff --git a/templates/application_view.html b/templates/application_view.html new file mode 100644 index 0000000..b05ee4b --- /dev/null +++ b/templates/application_view.html @@ -0,0 +1,259 @@ +{% extends "base.html" %} + +{% block title %}Bewerbung ansehen{% endblock %} + +{% block header %}Bewerbungsübersicht{% endblock %} + +{% block content %} +
+
+

Stelleninformation

+
+
+ Position: + {{ app_data.job_name }} +
+
+ E-Mail: + {{ app_data.email }} +
+
+ Status: + {{ app_data.status or 'In Bearbeitung' }} +
+
+ Eingereicht am: + + {% if app_data.submitted_at %} + {{ app_data.submitted_at[:10] }} {{ app_data.submitted_at[11:16] }} + {% else %} + Noch nicht eingereicht + {% endif %} + +
+
+
+ +
+

Persönliche Informationen

+ {% if app_data.personal_info %} +
+
+ Name: + {{ app_data.personal_info.name }} +
+
+ Vorname: + {{ app_data.personal_info.firstname }} +
+
+ Adresse: + {{ app_data.personal_info.address }} +
+
+ PLZ: + {{ app_data.personal_info.zip_code }} +
+
+ Stadt: + {{ app_data.personal_info.city }} +
+
+ Telefon: + {{ app_data.personal_info.phone }} +
+
+ Geburtsjahr: + {{ app_data.personal_info.birth_year }} +
+
+ Zivilstand: + {{ app_data.personal_info.civil_status or 'Nicht angegeben' }} +
+
+ {% else %} +

Noch nicht ausgefüllt

+ {% endif %} +
+ +
+

Motivation und Qualifikationen

+ {% if app_data.motivation_answers %} +
+
+

Aktuelle berufliche Situation

+

{{ app_data.motivation_answers.current_job or 'Nicht angegeben' }}

+
+
+

Motivation

+

{{ app_data.motivation_answers.motivation or 'Nicht angegeben' }}

+
+
+

Qualifikationen

+

{{ app_data.motivation_answers.qualifications or 'Nicht angegeben' }}

+
+
+

Gehaltsvorstellungen

+

{{ app_data.motivation_answers.salary or 'Nicht angegeben' }}

+
+
+ {% else %} +

Noch nicht ausgefüllt

+ {% endif %} +
+ +
+

Hochgeladene Dokumente

+ {% if app_data.uploaded_files %} +
+ + + + + + + + + + + {% for file in app_data.uploaded_files %} + + + + + + + {% endfor %} + +
DateinameGrößeHochgeladen amAktionen
{{ file.original_name }}{{ "%.2f"|format(file.size / 1024 / 1024) }} MB{{ file.uploaded_at[:10] }} {{ file.uploaded_at[11:16] }} + Herunterladen +
+
+ {% else %} +

Keine Dokumente hochgeladen

+ {% endif %} +
+ +
+

Zeitstempel

+
+
+ Erstellt am: + {{ app_data.created_at[:10] }} {{ app_data.created_at[11:16] }} +
+
+ Zuletzt aktualisiert: + {{ app_data.updated_at[:10] }} {{ app_data.updated_at[11:16] }} +
+
+
+
+ + +{% endblock %} diff --git a/tests/test_application_view.py b/tests/test_application_view.py new file mode 100644 index 0000000..f4559fe --- /dev/null +++ b/tests/test_application_view.py @@ -0,0 +1,354 @@ +""" +Tests for application view and file download functionality. +""" +import os +import pytest +from pathlib import Path + + +class TestApplicationView: + """Test application view page for HR.""" + + def test_view_application_page_renders(self, client, create_test_application): + """Test that the application view page renders correctly.""" + session_id = 'test-view-123' + create_test_application( + session_id=session_id, + email='test@example.com', + job_name='Software Engineer', + personal_info={ + 'name': 'Müller', + 'firstname': 'Anna', + 'address': 'Hauptstrasse 123', + 'zip_code': '8001', + 'city': 'Zürich', + 'phone': '+41 79 123 45 67', + 'birth_year': '1990', + 'civil_status': 'ledig' + }, + motivation_answers={ + 'current_job': 'Software Developer', + 'motivation': 'Great opportunity', + 'qualifications': 'Python, Flask', + 'salary': '100000 CHF' + } + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + assert b'Software Engineer' in response.data + assert b'test@example.com' in response.data + assert b'Anna' in response.data + assert b'Great opportunity' in response.data + + def test_view_application_404_for_invalid_session(self, client): + """Test that viewing a non-existent application returns 404.""" + response = client.get('/application/invalid-session-id/') + assert response.status_code == 404 + + def test_view_application_displays_all_personal_info(self, client, create_test_application): + """Test that all personal information fields are displayed.""" + session_id = 'test-personal-display' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Data Analyst', + personal_info={ + 'name': 'Schmidt', + 'firstname': 'Max', + 'address': 'Bahnhofstrasse 1', + 'zip_code': '8000', + 'city': 'Zürich', + 'phone': '+41 79 999 88 77', + 'birth_year': '1985', + 'civil_status': 'verheiratet' + } + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + assert b'Schmidt' in response.data + assert b'Max' in response.data + assert b'Bahnhofstrasse 1' in response.data + assert b'8000' in response.data + assert b'verheiratet' in response.data + + def test_view_application_displays_motivation_answers(self, client, create_test_application): + """Test that motivation answers are properly displayed.""" + session_id = 'test-motivation-display' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Project Manager', + motivation_answers={ + 'current_job': 'Senior Developer at ABC Corp', + 'motivation': 'I want to work on challenging projects', + 'qualifications': '10 years of experience in software development', + 'salary': '120000 CHF per year' + } + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + assert b'Senior Developer at ABC Corp' in response.data + assert b'challenging projects' in response.data + assert b'10 years of experience' in response.data + assert b'120000 CHF' in response.data + + def test_view_application_with_uploaded_files( + self, client, create_test_application, temp_applications_dir + ): + """Test that uploaded files are displayed in the application view.""" + session_id = 'test-files-display' + + # Create the application with uploaded files + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='UI Designer', + uploaded_files=[ + { + 'original_name': 'cv.pdf', + 'stored_name': '20250101_120000_cv.pdf', + 'size': 102400, + 'uploaded_at': '2025-01-01T12:00:00' + }, + { + 'original_name': 'portfolio.pdf', + 'stored_name': '20250101_120100_portfolio.pdf', + 'size': 204800, + 'uploaded_at': '2025-01-01T12:01:00' + } + ] + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + assert b'cv.pdf' in response.data + assert b'portfolio.pdf' in response.data + # Check that file sizes are displayed (in MB) + assert b'0.10 MB' in response.data + assert b'0.20 MB' in response.data + + def test_view_application_without_uploaded_files(self, client, create_test_application): + """Test that application view works when no files are uploaded.""" + session_id = 'test-no-files' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Sales Representative', + uploaded_files=[] + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + assert b'Keine Dokumente hochgeladen' in response.data + + def test_view_application_shows_timestamps(self, client, create_test_application): + """Test that creation and update timestamps are displayed.""" + session_id = 'test-timestamps' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Marketing Manager' + ) + + response = client.get(f'/application/{session_id}/') + + assert response.status_code == 200 + # Check for timestamp labels in German + assert b'Erstellt am:' in response.data + assert b'Zuletzt aktualisiert:' in response.data + + +class TestFileDownload: + """Test file download functionality.""" + + def test_download_file_success(self, client, create_test_application, temp_applications_dir): + """Test that files can be successfully downloaded.""" + session_id = 'test-download-success' + + # Create application directory and attachment + from app import get_attachments_path + attachments_path = get_attachments_path(session_id) + Path(attachments_path).mkdir(parents=True, exist_ok=True) + + # Create a test file + test_filename = '20250101_120000_test.pdf' + test_file_path = os.path.join(attachments_path, test_filename) + test_content = b'Test PDF content' + + with open(test_file_path, 'wb') as f: + f.write(test_content) + + # Create application with the uploaded file + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Job', + uploaded_files=[ + { + 'original_name': 'test.pdf', + 'stored_name': test_filename, + 'size': len(test_content), + 'uploaded_at': '2025-01-01T12:00:00' + } + ] + ) + + # Download the file + response = client.get(f'/application/{session_id}/download/{test_filename}') + + assert response.status_code == 200 + assert response.data == test_content + assert 'attachment' in response.headers.get('Content-Disposition', '') + + def test_download_file_not_in_uploaded_list( + self, client, create_test_application, temp_applications_dir + ): + """Test that downloading a file not in the uploaded list returns 404.""" + session_id = 'test-download-unauthorized' + + # Create application directory and attachment + from app import get_attachments_path + attachments_path = get_attachments_path(session_id) + Path(attachments_path).mkdir(parents=True, exist_ok=True) + + # Create a file that exists physically but is NOT in the uploaded_files list + unauthorized_filename = '20250101_120000_unauthorized.pdf' + unauthorized_path = os.path.join(attachments_path, unauthorized_filename) + + with open(unauthorized_path, 'wb') as f: + f.write(b'Unauthorized content') + + # Create application with NO uploaded files + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Job', + uploaded_files=[] + ) + + # Try to download the unauthorized file + response = client.get(f'/application/{session_id}/download/{unauthorized_filename}') + + assert response.status_code == 404 + + def test_download_file_nonexistent_file(self, client, create_test_application): + """Test that downloading a non-existent file returns 404.""" + session_id = 'test-download-nonexistent' + + # Create application with a file in the list, but the file doesn't exist physically + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Job', + uploaded_files=[ + { + 'original_name': 'missing.pdf', + 'stored_name': 'nonexistent_file.pdf', + 'size': 1024, + 'uploaded_at': '2025-01-01T12:00:00' + } + ] + ) + + # Try to download the missing file + response = client.get(f'/application/{session_id}/download/nonexistent_file.pdf') + + assert response.status_code == 404 + + def test_download_file_invalid_session(self, client): + """Test that downloading with an invalid session ID returns 404.""" + response = client.get('/application/invalid-session/download/somefile.pdf') + assert response.status_code == 404 + + def test_download_multiple_files(self, client, create_test_application, temp_applications_dir): + """Test downloading multiple files from the same application.""" + session_id = 'test-download-multiple' + + # Create application directory + from app import get_attachments_path + attachments_path = get_attachments_path(session_id) + Path(attachments_path).mkdir(parents=True, exist_ok=True) + + # Create multiple test files + files = [ + ('20250101_120000_cv.pdf', b'CV content'), + ('20250101_120100_cover_letter.pdf', b'Cover letter content'), + ('20250101_120200_certificate.pdf', b'Certificate content') + ] + + uploaded_files = [] + for filename, content in files: + file_path = os.path.join(attachments_path, filename) + with open(file_path, 'wb') as f: + f.write(content) + + uploaded_files.append({ + 'original_name': filename.split('_', 2)[2], + 'stored_name': filename, + 'size': len(content), + 'uploaded_at': '2025-01-01T12:00:00' + }) + + # Create application with multiple files + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Job', + uploaded_files=uploaded_files + ) + + # Download each file and verify + for filename, expected_content in files: + response = client.get(f'/application/{session_id}/download/{filename}') + assert response.status_code == 200 + assert response.data == expected_content + + def test_download_file_with_special_characters( + self, client, create_test_application, temp_applications_dir + ): + """Test downloading files with special characters in the name.""" + session_id = 'test-download-special-chars' + + # Create application directory + from app import get_attachments_path + attachments_path = get_attachments_path(session_id) + Path(attachments_path).mkdir(parents=True, exist_ok=True) + + # Create a file with a secure filename (as it would be stored) + from werkzeug.utils import secure_filename + original_name = 'Lebenslauf Müller.pdf' + stored_name = f'20250101_120000_{secure_filename(original_name)}' + file_path = os.path.join(attachments_path, stored_name) + content = b'CV with special chars' + + with open(file_path, 'wb') as f: + f.write(content) + + # Create application + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Job', + uploaded_files=[ + { + 'original_name': original_name, + 'stored_name': stored_name, + 'size': len(content), + 'uploaded_at': '2025-01-01T12:00:00' + } + ] + ) + + # Download the file + response = client.get(f'/application/{session_id}/download/{stored_name}') + + assert response.status_code == 200 + assert response.data == content diff --git a/tests/test_hr_notifications.py b/tests/test_hr_notifications.py new file mode 100644 index 0000000..54ce061 --- /dev/null +++ b/tests/test_hr_notifications.py @@ -0,0 +1,270 @@ +""" +Tests for HR email notifications functionality. +""" +import pytest +from unittest.mock import patch, MagicMock +from app import send_hr_notification + + +class TestHRNotifications: + """Test HR notification email functionality.""" + + def test_hr_notification_sent_on_submission(self, client, create_test_application, mock_mail): + """Test that HR receives email when application is submitted.""" + # Set HR_EMAIL in config + from app import app + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000' + + # Create a test application with all required data + session_id = 'test-session-123' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Software Developer', + current_page=4, + personal_info={ + 'name': 'Müller', + 'firstname': 'Anna', + 'address': 'Hauptstrasse 123', + 'zip_code': '8001', + 'city': 'Zürich', + 'phone': '+41 79 123 45 67', + 'birth_year': '1990', + 'civil_status': 'ledig' + }, + motivation_answers={ + 'current_job': 'Developer', + 'motivation': 'Great company', + 'qualifications': 'Python expert', + 'salary': '100000' + } + ) + + # Submit the application + response = client.post(f'/apply/{session_id}/submit-application') + + # Verify redirect to confirmation page + assert response.status_code == 302 + assert f'/apply/{session_id}/confirmation' in response.location + + # Verify HR email was sent + assert len(mock_mail.sent_messages) == 1 + hr_email = mock_mail.sent_messages[0] + + assert 'hr@example.com' in hr_email.recipients + assert 'Neue Bewerbung eingegangen: Software Developer' == hr_email.subject + assert 'Anna Müller' in hr_email.body + assert 'applicant@example.com' in hr_email.body + assert f'/application/{session_id}/' in hr_email.body + + def test_hr_notification_not_sent_when_hr_email_not_configured( + self, client, create_test_application, mock_mail, caplog + ): + """Test that HR notification is skipped when HR_EMAIL is not configured.""" + # Ensure HR_EMAIL is not set + from app import app + app.config['HR_EMAIL'] = None + + # Create a test application + session_id = 'test-session-456' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Data Analyst', + current_page=4, + personal_info={ + 'name': 'Schmidt', + 'firstname': 'Max', + 'address': 'Bahnhofstrasse 1', + 'zip_code': '8000', + 'city': 'Zürich', + 'phone': '+41 79 999 88 77', + 'birth_year': '1985', + 'civil_status': 'verheiratet' + } + ) + + # Submit the application + response = client.post(f'/apply/{session_id}/submit-application') + + # Verify redirect to confirmation page + assert response.status_code == 302 + + # Verify no HR email was sent + assert len(mock_mail.sent_messages) == 0 + + # Verify warning was logged + assert 'HR_EMAIL not configured' in caplog.text + + def test_hr_notification_includes_file_count(self, client, create_test_application, mock_mail): + """Test that HR notification includes the count of uploaded files.""" + from app import app + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000' + + # Create application with uploaded files + session_id = 'test-session-789' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Project Manager', + current_page=4, + personal_info={ + 'name': 'Weber', + 'firstname': 'Lisa', + 'address': 'Seestrasse 45', + 'zip_code': '8002', + 'city': 'Zürich', + 'phone': '+41 79 555 44 33', + 'birth_year': '1992', + 'civil_status': 'ledig' + }, + uploaded_files=[ + { + 'original_name': 'cv.pdf', + 'stored_name': '20250101_120000_cv.pdf', + 'size': 102400, + 'uploaded_at': '2025-01-01T12:00:00' + }, + { + 'original_name': 'certificate.pdf', + 'stored_name': '20250101_120100_certificate.pdf', + 'size': 204800, + 'uploaded_at': '2025-01-01T12:01:00' + } + ] + ) + + # Submit the application + response = client.post(f'/apply/{session_id}/submit-application') + + assert response.status_code == 302 + + # Verify HR email contains file count + assert len(mock_mail.sent_messages) == 1 + hr_email = mock_mail.sent_messages[0] + assert 'Anzahl der hochgeladenen Dokumente: 2' in hr_email.body + + def test_hr_notification_with_zero_files(self, client, create_test_application, mock_mail): + """Test that HR notification works even when no files are uploaded.""" + from app import app + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000' + + # Create application without uploaded files + session_id = 'test-session-000' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Sales Representative', + current_page=4, + personal_info={ + 'name': 'Fischer', + 'firstname': 'Tom', + 'address': 'Hauptplatz 10', + 'zip_code': '8003', + 'city': 'Zürich', + 'phone': '+41 79 111 22 33', + 'birth_year': '1988', + 'civil_status': 'ledig' + }, + uploaded_files=[] + ) + + # Submit the application + response = client.post(f'/apply/{session_id}/submit-application') + + assert response.status_code == 302 + + # Verify HR email was sent with 0 file count + assert len(mock_mail.sent_messages) == 1 + hr_email = mock_mail.sent_messages[0] + assert 'Anzahl der hochgeladenen Dokumente: 0' in hr_email.body + + def test_send_hr_notification_function_directly(self, app): + """Test the send_hr_notification function directly.""" + from app import send_hr_notification + + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000' + + session_id = 'test-123' + app_data = { + 'email': 'applicant@example.com', + 'job_name': 'Test Job', + 'submitted_at': '2025-01-01T12:00:00', + 'personal_info': { + 'firstname': 'John', + 'name': 'Doe' + }, + 'uploaded_files': [ + {'original_name': 'cv.pdf', 'stored_name': 'cv_stored.pdf', 'size': 1024} + ] + } + + with app.app_context(): + # Mock mail.send to prevent actual sending + with patch('app.mail.send') as mock_send: + result = send_hr_notification(session_id, app_data) + assert result is True + assert mock_send.called + + def test_send_hr_notification_handles_email_failure(self, app, caplog): + """Test that send_hr_notification handles email sending failures gracefully.""" + from app import send_hr_notification + + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000' + + session_id = 'test-456' + app_data = { + 'email': 'applicant@example.com', + 'job_name': 'Test Job', + 'submitted_at': '2025-01-01T12:00:00', + 'personal_info': { + 'firstname': 'Jane', + 'name': 'Smith' + }, + 'uploaded_files': [] + } + + with app.app_context(): + # Mock mail.send to raise an exception + with patch('app.mail.send', side_effect=Exception('SMTP error')): + result = send_hr_notification(session_id, app_data) + assert result is False + assert 'Failed to send HR notification email' in caplog.text + + def test_hr_notification_url_format(self, client, create_test_application, mock_mail): + """Test that the application URL in HR notification is correctly formatted.""" + from app import app + app.config['HR_EMAIL'] = 'hr@example.com' + app.config['APPLICATION_URL_BASE'] = 'https://example.com' + + session_id = 'test-url-format' + create_test_application( + session_id=session_id, + email='applicant@example.com', + job_name='Test Position', + current_page=4, + personal_info={ + 'name': 'Test', + 'firstname': 'User', + 'address': 'Street 1', + 'zip_code': '8000', + 'city': 'City', + 'phone': '+41 79 123 45 67', + 'birth_year': '1990', + 'civil_status': 'ledig' + } + ) + + # Submit the application + client.post(f'/apply/{session_id}/submit-application') + + # Verify URL format in email + assert len(mock_mail.sent_messages) == 1 + hr_email = mock_mail.sent_messages[0] + expected_url = f'https://example.com/application/{session_id}/' + assert expected_url in hr_email.body