Tests added

This commit is contained in:
2025-12-27 16:49:52 +01:00
parent 2284808043
commit 3f26416bdd
15 changed files with 1776 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Tests package
+161
View File
@@ -0,0 +1,161 @@
"""
Pytest configuration and shared fixtures for the Flask job application tests.
"""
import os
import tempfile
import shutil
from io import BytesIO
from datetime import datetime
import pytest
import yaml
from app import app as flask_app
from flask_mail import Mail
@pytest.fixture
def app():
"""Create and configure a test Flask application instance."""
# Create a temporary directory for test applications
temp_dir = tempfile.mkdtemp()
# Configure app for testing
flask_app.config['TESTING'] = True
flask_app.config['SECRET_KEY'] = 'test-secret-key'
flask_app.config['APPLICATIONS_FOLDER'] = temp_dir
flask_app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF for testing
flask_app.config['MAIL_SUPPRESS_SEND'] = True # Don't actually send emails
yield flask_app
# Cleanup: remove temporary directory
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
@pytest.fixture
def client(app):
"""Create a test client for making requests to the application."""
return app.test_client()
@pytest.fixture
def runner(app):
"""Create a test CLI runner."""
return app.test_cli_runner()
@pytest.fixture
def temp_applications_dir(app):
"""Get the temporary applications directory for the current test."""
return app.config['APPLICATIONS_FOLDER']
@pytest.fixture
def sample_application_data():
"""Sample application data for testing."""
return {
'session_id': 'test-session-123',
'email': 'test@example.com',
'job_name': 'Test Position',
'current_page': 2,
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(),
'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 Entwicklerin',
'motivation': 'Ich möchte in einem innovativen Team arbeiten',
'qualifications': 'Python, Flask, 5 Jahre Erfahrung',
'salary': '80000 CHF'
},
'uploaded_files': []
}
@pytest.fixture
def sample_pdf_file():
"""Create a sample PDF file for testing uploads."""
# Create a minimal PDF file
pdf_content = b'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000056 00000 n\n0000000115 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n190\n%%EOF'
return BytesIO(pdf_content)
@pytest.fixture
def sample_jpg_file():
"""Create a sample JPG file for testing uploads."""
# Create a minimal JPEG file (1x1 pixel red image)
jpg_content = (
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
b'\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c'
b'\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c'
b'\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xc0\x00\x0b\x08\x00'
b'\x01\x00\x01\x01\x01\x11\x00\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01'
b'\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05'
b'\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04'
b'\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A'
b'\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82'
b'\t\n\x16\x17\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxy'
b'z\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a'
b'\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9'
b'\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8'
b'\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5'
b'\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xfb\xfe\x87'
b'\xff\xd9'
)
return BytesIO(jpg_content)
@pytest.fixture
def large_file():
"""Create a file larger than the 4MB limit for testing."""
# Create a 5MB file
content = b'x' * (5 * 1024 * 1024)
return BytesIO(content)
@pytest.fixture
def mock_mail(app, monkeypatch):
"""Mock Flask-Mail to prevent actual email sending."""
class MockMail:
def __init__(self):
self.sent_messages = []
def send(self, message):
self.sent_messages.append(message)
mock = MockMail()
monkeypatch.setattr('app.mail', mock)
return mock
@pytest.fixture
def create_test_application(app, temp_applications_dir):
"""Factory fixture to create test application data."""
def _create(session_id='test-123', **kwargs):
"""Create a test application with the given session ID and data."""
from app import save_application_data
data = {
'session_id': session_id,
'email': kwargs.get('email', 'test@example.com'),
'job_name': kwargs.get('job_name', 'Test Job'),
'current_page': kwargs.get('current_page', 2),
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(),
'personal_info': kwargs.get('personal_info', {}),
'motivation_answers': kwargs.get('motivation_answers', {}),
'uploaded_files': kwargs.get('uploaded_files', [])
}
save_application_data(session_id, data)
return data
return _create
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

+12
View File
@@ -0,0 +1,12 @@
%PDF-1.4
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000056 00000 n
0000000115 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
190
%%EOF
+91
View File
@@ -0,0 +1,91 @@
"""
Tests for configuration settings and helper functions.
"""
import pytest
import os
from config import Config
from app import get_application_path, get_data_file_path, get_attachments_path
class TestConfiguration:
"""Tests for Config class."""
def test_config_has_required_settings(self):
"""Test that all required configuration values exist."""
assert hasattr(Config, 'SECRET_KEY')
assert hasattr(Config, 'MAIL_SERVER')
assert hasattr(Config, 'MAIL_PORT')
assert hasattr(Config, 'MAIL_USE_TLS')
assert hasattr(Config, 'APPLICATIONS_FOLDER')
assert hasattr(Config, 'MAX_FILE_SIZE')
assert hasattr(Config, 'MAX_FILES')
assert hasattr(Config, 'ALLOWED_EXTENSIONS')
assert hasattr(Config, 'MAX_STRING_LENGTH')
assert hasattr(Config, 'MAX_TEXT_AREA_LENGTH')
assert hasattr(Config, 'MAX_ZIP_DIGITS')
assert hasattr(Config, 'MIN_BIRTH_YEAR')
assert hasattr(Config, 'MAX_BIRTH_YEAR')
def test_file_size_limit(self):
"""Test that file size limit is set correctly."""
assert Config.MAX_FILE_SIZE == 4 * 1024 * 1024 # 4 MB
def test_max_files_limit(self):
"""Test that maximum number of files is set correctly."""
assert Config.MAX_FILES == 3
def test_allowed_extensions(self):
"""Test that allowed file extensions are configured."""
assert isinstance(Config.ALLOWED_EXTENSIONS, set)
assert 'pdf' in Config.ALLOWED_EXTENSIONS
assert 'doc' in Config.ALLOWED_EXTENSIONS
assert 'docx' in Config.ALLOWED_EXTENSIONS
def test_string_length_limits(self):
"""Test that string length limits are set."""
assert Config.MAX_STRING_LENGTH == 255
assert Config.MAX_TEXT_AREA_LENGTH == 3000
assert Config.MAX_ZIP_DIGITS == 10
def test_birth_year_range(self):
"""Test that birth year range is configured correctly."""
assert Config.MIN_BIRTH_YEAR == 1940
assert Config.MAX_BIRTH_YEAR == 2010
assert Config.MIN_BIRTH_YEAR < Config.MAX_BIRTH_YEAR
class TestPathHelpers:
"""Tests for path helper functions."""
def test_get_application_path(self, app):
"""Test get_application_path returns correct path."""
session_id = 'test-123'
expected_path = os.path.join(app.config['APPLICATIONS_FOLDER'], session_id)
assert get_application_path(session_id) == expected_path
def test_get_data_file_path(self, app):
"""Test get_data_file_path returns correct YAML file path."""
session_id = 'test-456'
expected_path = os.path.join(
app.config['APPLICATIONS_FOLDER'],
session_id,
'data.yaml'
)
assert get_data_file_path(session_id) == expected_path
def test_get_attachments_path(self, app):
"""Test get_attachments_path returns correct attachments directory path."""
session_id = 'test-789'
expected_path = os.path.join(
app.config['APPLICATIONS_FOLDER'],
session_id,
'attachments'
)
assert get_attachments_path(session_id) == expected_path
def test_path_helpers_with_special_characters(self, app):
"""Test path helpers handle session IDs with special characters."""
session_id = 'test-abc-123-def'
app_path = get_application_path(session_id)
assert session_id in app_path
assert os.path.isabs(app_path) or app_path.startswith('.')
+91
View File
@@ -0,0 +1,91 @@
"""
Tests for email functionality.
"""
import pytest
from unittest.mock import patch, MagicMock
from flask_mail import Message
from app import send_resume_email
class TestEmailSending:
"""Tests for email sending functionality."""
def test_send_resume_email_success(self, app):
"""Test successfully sending a resume email."""
with patch('app.mail.send') as mock_send:
result = send_resume_email('test@example.com', 'test-session-123', 'Software Developer')
assert result is True
assert mock_send.called
def test_send_resume_email_failure(self, app):
"""Test handling email sending failure."""
with patch('app.mail.send') as mock_send:
mock_send.side_effect = Exception('SMTP error')
result = send_resume_email('test@example.com', 'test-session-123', 'Test Job')
assert result is False
def test_resume_email_content(self, app):
"""Test that resume email contains correct content."""
with patch('app.mail.send') as mock_send:
send_resume_email('applicant@example.com', 'abc-123', 'Marketing Manager')
# Get the Message object that was passed to send()
assert mock_send.called
call_args = mock_send.call_args
message = call_args[0][0] if call_args[0] else None
if message:
assert isinstance(message, Message)
assert 'applicant@example.com' in message.recipients
assert 'Bewerbung' in message.subject or 'Marketing Manager' in message.subject
def test_resume_link_in_email(self, app):
"""Test that resume link is included in email body."""
with patch('app.mail.send') as mock_send:
send_resume_email('test@example.com', 'session-xyz', 'Test Position')
if mock_send.called:
call_args = mock_send.call_args
message = call_args[0][0] if call_args[0] else None
if message and hasattr(message, 'body'):
assert 'session-xyz' in message.body or '/resume/' in message.body
class TestEmailIntegration:
"""Tests for email integration with application workflow."""
def test_email_sent_on_initial_submission(self, client, app):
"""Test that email is sent when user submits their email address."""
with patch('app.mail.send') as mock_send:
response = client.post('/apply/submit-email', data={
'email': 'newuser@example.com',
'job_name': 'Junior Developer'
}, follow_redirects=True)
assert response.status_code == 200
# Email should be sent
assert mock_send.called or app.config.get('MAIL_SUPPRESS_SEND') is True
def test_email_suppressed_in_testing(self, app):
"""Test that email is suppressed during testing."""
assert app.config['TESTING'] is True
assert app.config.get('MAIL_SUPPRESS_SEND') is True
class TestEmailConfiguration:
"""Tests for email configuration."""
def test_email_config_exists(self, app):
"""Test that email configuration is set."""
assert 'MAIL_SERVER' in app.config
assert 'MAIL_PORT' in app.config
assert 'MAIL_USE_TLS' in app.config
def test_mail_suppress_send_in_testing(self, app):
"""Test that email sending is suppressed in test mode."""
assert app.config['TESTING'] is True
assert app.config['MAIL_SUPPRESS_SEND'] is True
+325
View File
@@ -0,0 +1,325 @@
"""
Integration tests for the complete application workflow.
"""
import pytest
from unittest.mock import patch
from app import load_application_data
@pytest.mark.integration
class TestCompleteWorkflow:
"""Integration tests for the complete application workflow."""
def test_complete_application_workflow(self, client, app, sample_pdf_file):
"""Test the complete workflow from email to confirmation."""
# Step 1: Submit email
with patch('app.mail.send'):
response = client.post('/apply/submit-email', data={
'email': 'integration@test.com',
'job_name': 'Integration Test Position'
}, follow_redirects=True)
assert response.status_code == 200
# For testing, create a known session
from app import save_application_data
session_id = 'integration-test-123'
save_application_data(session_id, {
'session_id': session_id,
'email': 'integration@test.com',
'job_name': 'Integration Test Position',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Step 2: Submit personal information
response = client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Schmidt',
'firstname': 'Thomas',
'address': 'Bahnhofstrasse 100',
'zip_code': '8001',
'city': 'Zürich',
'phone': '+41 79 999 88 77',
'birth_year': '1985',
'civil_status': 'verheiratet'
}, follow_redirects=True)
assert response.status_code == 200
# Verify data was saved
data = load_application_data(session_id)
assert data['personal_info']['name'] == 'Schmidt'
assert data['current_page'] == 3
# Step 3: Submit motivation answers
response = client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': 'Senior Software Entwickler bei Tech Corp',
'motivation': 'Ich möchte Teil eines innovativen Teams werden',
'qualifications': 'Python, Flask, React, 10 Jahre Erfahrung',
'salary': '120000 CHF pro Jahr'
}, follow_redirects=True)
assert response.status_code == 200
# Verify motivation was saved
data = load_application_data(session_id)
assert data['motivation_answers']['current_job'] == 'Senior Software Entwickler bei Tech Corp'
assert data['current_page'] == 4
# Step 4: Upload a file
sample_pdf_file.seek(0)
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'lebenslauf.pdf')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
# Verify file was uploaded
data = load_application_data(session_id)
assert len(data['uploaded_files']) == 1
assert data['uploaded_files'][0]['original_name'] == 'lebenslauf.pdf'
# Step 5: Submit final application
response = client.post(f'/apply/{session_id}/submit-application', follow_redirects=True)
assert response.status_code == 200
assert b'Vielen Dank' in response.data or b'erfolgreich' in response.data
# Verify application was marked as submitted
data = load_application_data(session_id)
assert data['current_page'] == 5
assert data.get('status') == 'submitted'
def test_workflow_with_resume(self, client, app, create_test_application):
"""Test resuming an application at different stages."""
session_id = 'resume-test-456'
# Create an application at page 2 (personal info)
create_test_application(
session_id=session_id,
current_page=2,
email='resume@test.com',
job_name='Resume Test Job'
)
# Resume should redirect to personal info page
response = client.get(f'/resume/{session_id}', follow_redirects=False)
assert response.status_code == 302
assert b'/personal' in response.data
# Fill in personal info
client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Weber',
'firstname': 'Maria',
'address': 'Seestrasse 50',
'zip_code': '8002',
'city': 'Zürich',
'phone': '+41 79 888 77 66',
'birth_year': '1992'
})
# Now resume should go to motivation page
response = client.get(f'/resume/{session_id}', follow_redirects=False)
assert response.status_code == 302
assert b'/motivation' in response.data
def test_multiple_concurrent_applications(self, client, app):
"""Test handling multiple concurrent applications with different sessions."""
from app import save_application_data
# Create multiple applications
sessions = []
for i in range(3):
session_id = f'concurrent-{i}'
sessions.append(session_id)
save_application_data(session_id, {
'session_id': session_id,
'email': f'user{i}@test.com',
'job_name': f'Job {i}',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Verify all applications are independent
for i, session_id in enumerate(sessions):
data = load_application_data(session_id)
assert data is not None
assert data['email'] == f'user{i}@test.com'
assert data['job_name'] == f'Job {i}'
# Update one application
client.post(f'/apply/{sessions[1]}/submit-personal', data={
'name': 'Test',
'firstname': 'User',
'address': 'Street 1',
'zip_code': '12345',
'city': 'City',
'phone': '+41 79 123 45 67',
'birth_year': '1990'
})
# Verify only that application was updated
data0 = load_application_data(sessions[0])
data1 = load_application_data(sessions[1])
data2 = load_application_data(sessions[2])
assert data0['current_page'] == 2 # Not updated
assert data1['current_page'] == 3 # Updated
assert data2['current_page'] == 2 # Not updated
@pytest.mark.integration
class TestWorkflowEdgeCases:
"""Integration tests for edge cases in the workflow."""
def test_skip_optional_fields(self, client, app):
"""Test completing workflow without filling optional fields."""
from app import save_application_data
session_id = 'optional-test'
save_application_data(session_id, {
'session_id': session_id,
'email': 'optional@test.com',
'job_name': 'Test Job',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Submit personal info with minimal fields
client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Min',
'firstname': 'User',
'address': 'Addr 1',
'zip_code': '1',
'city': 'City',
'phone': '+1 123',
'birth_year': '1990',
'civil_status': '' # Optional, empty
})
# Submit empty motivation (all fields optional)
client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': '',
'motivation': '',
'qualifications': '',
'salary': ''
})
# Submit without files (optional)
response = client.post(f'/apply/{session_id}/submit-application', follow_redirects=True)
assert response.status_code == 200
# Verify application was submitted
data = load_application_data(session_id)
assert data['status'] == 'submitted'
def test_workflow_with_unicode_data(self, client, app):
"""Test workflow with German special characters."""
from app import save_application_data
session_id = 'unicode-test'
save_application_data(session_id, {
'session_id': session_id,
'email': 'ümlaut@test.com',
'job_name': 'Geschäftsführer Position',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Submit with German characters
client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Müller',
'firstname': 'Björn',
'address': 'Königstrasse 42',
'zip_code': '80539',
'city': 'München',
'phone': '+49 89 123 45 67',
'birth_year': '1988'
})
# Verify data persisted correctly
data = load_application_data(session_id)
assert data['personal_info']['name'] == 'Müller'
assert data['personal_info']['firstname'] == 'Björn'
assert data['personal_info']['city'] == 'München'
assert data['job_name'] == 'Geschäftsführer Position'
@pytest.mark.integration
@pytest.mark.slow
class TestDataIntegrity:
"""Integration tests for data integrity throughout the workflow."""
def test_data_not_lost_between_pages(self, client, app):
"""Test that data persists correctly when navigating between pages."""
from app import save_application_data
session_id = 'persistence-test'
save_application_data(session_id, {
'session_id': session_id,
'email': 'persist@test.com',
'job_name': 'Persistence Job',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Add personal info
personal_data = {
'name': 'Persistence',
'firstname': 'Test',
'address': 'Test Street 1',
'zip_code': '12345',
'city': 'Test City',
'phone': '+1 555 1234',
'birth_year': '1990'
}
client.post(f'/apply/{session_id}/submit-personal', data=personal_data)
# Verify personal data
data = load_application_data(session_id)
for key, value in personal_data.items():
assert data['personal_info'][key] == value
# Add motivation
motivation_data = {
'current_job': 'Developer',
'motivation': 'Test motivation',
'qualifications': 'Test skills',
'salary': '100k'
}
client.post(f'/apply/{session_id}/submit-motivation', data=motivation_data)
# Verify both personal and motivation data still exist
data = load_application_data(session_id)
for key, value in personal_data.items():
assert data['personal_info'][key] == value
for key, value in motivation_data.items():
assert data['motivation_answers'][key] == value
# Submit application
client.post(f'/apply/{session_id}/submit-application')
# Verify all data still intact after submission
data = load_application_data(session_id)
assert data['email'] == 'persist@test.com'
for key, value in personal_data.items():
assert data['personal_info'][key] == value
for key, value in motivation_data.items():
assert data['motivation_answers'][key] == value
+248
View File
@@ -0,0 +1,248 @@
"""
Tests for routes and form submissions in the Flask job application system.
"""
import pytest
from flask import session
from app import load_application_data
class TestPage1Email:
"""Tests for Page 1: Email capture."""
def test_page1_loads(self, client):
"""Test that page 1 loads successfully."""
response = client.get('/apply?job=TestJob')
assert response.status_code == 200
assert b'Bewerbung' in response.data
assert b'TestJob' in response.data
def test_page1_default_job_name(self, client):
"""Test page 1 with no job parameter."""
response = client.get('/apply')
assert response.status_code == 200
assert b'Offene Position' in response.data
def test_submit_valid_email(self, client, app):
"""Test submitting a valid email address."""
with app.app_context():
response = client.post('/apply/submit-email', data={
'email': 'test@example.com',
'job_name': 'Software Developer'
}, follow_redirects=False)
assert response.status_code == 302 # Redirect
assert b'/apply/' in response.data # Redirects to page 2
def test_submit_invalid_email(self, client):
"""Test submitting an invalid email address."""
response = client.post('/apply/submit-email', data={
'email': 'invalid-email',
'job_name': 'Test Job'
}, follow_redirects=True)
assert b'E-Mail' in response.data or b'email' in response.data
class TestPage2Personal:
"""Tests for Page 2: Personal information."""
def test_page2_loads(self, client, create_test_application):
"""Test that page 2 loads successfully."""
session_id = 'test-page2-123'
create_test_application(session_id=session_id)
response = client.get(f'/apply/{session_id}/personal')
assert response.status_code == 200
assert b'Pers' in response.data or b'Name' in response.data
def test_page2_nonexistent_session(self, client):
"""Test page 2 with non-existent session."""
response = client.get('/apply/nonexistent/personal', follow_redirects=True)
assert response.status_code == 200
# Should redirect to page 1
def test_submit_valid_personal_info(self, client, create_test_application):
"""Test submitting valid personal information."""
session_id = 'test-personal-valid'
create_test_application(session_id=session_id, current_page=2)
response = client.post(f'/apply/{session_id}/submit-personal', data={
'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'
}, follow_redirects=False)
assert response.status_code == 302 # Redirect to page 3
assert b'/motivation' in response.data
def test_submit_missing_required_fields(self, client, create_test_application):
"""Test submitting personal info with missing required fields."""
session_id = 'test-personal-missing'
create_test_application(session_id=session_id, current_page=2)
response = client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Test',
# Missing firstname, address, etc.
}, follow_redirects=True)
assert response.status_code == 200
# Should show error messages
def test_submit_invalid_phone(self, client, create_test_application):
"""Test submitting with invalid phone number."""
session_id = 'test-personal-phone'
create_test_application(session_id=session_id, current_page=2)
response = client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Test',
'firstname': 'User',
'address': 'Street 1',
'zip_code': '12345',
'city': 'City',
'phone': '123456789', # Missing + prefix
'birth_year': '1990'
}, follow_redirects=True)
assert response.status_code == 200
class TestPage3Motivation:
"""Tests for Page 3: Motivation questions."""
def test_page3_loads(self, client, create_test_application):
"""Test that page 3 loads successfully."""
session_id = 'test-page3-123'
create_test_application(session_id=session_id, current_page=3)
response = client.get(f'/apply/{session_id}/motivation')
assert response.status_code == 200
assert b'Motivation' in response.data or b'berufliche' in response.data
def test_submit_valid_motivation(self, client, create_test_application):
"""Test submitting valid motivation answers."""
session_id = 'test-motivation-valid'
create_test_application(session_id=session_id, current_page=3)
response = client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': 'Software Entwickler',
'motivation': 'Ich möchte in einem innovativen Team arbeiten',
'qualifications': 'Python, Flask, 5 Jahre Erfahrung',
'salary': '80000 CHF'
}, follow_redirects=False)
assert response.status_code == 302
assert b'/upload' in response.data
def test_submit_empty_motivation(self, client, create_test_application):
"""Test submitting empty motivation (all optional)."""
session_id = 'test-motivation-empty'
create_test_application(session_id=session_id, current_page=3)
response = client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': '',
'motivation': '',
'qualifications': '',
'salary': ''
}, follow_redirects=False)
assert response.status_code == 302 # Should still work
def test_submit_too_long_text(self, client, create_test_application):
"""Test submitting text that exceeds the character limit."""
session_id = 'test-motivation-long'
create_test_application(session_id=session_id, current_page=3)
long_text = 'x' * 3001 # Exceeds 3000 character limit
response = client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': long_text,
'motivation': '',
'qualifications': '',
'salary': ''
}, follow_redirects=True)
assert response.status_code == 200
# Should show error about character limit
class TestPage4Upload:
"""Tests for Page 4: File upload."""
def test_page4_loads(self, client, create_test_application):
"""Test that page 4 loads successfully."""
session_id = 'test-page4-123'
create_test_application(session_id=session_id, current_page=4)
response = client.get(f'/apply/{session_id}/upload')
assert response.status_code == 200
assert b'Dokument' in response.data or b'hochladen' in response.data
def test_submit_application(self, client, create_test_application):
"""Test submitting the final application."""
session_id = 'test-submit-app'
create_test_application(session_id=session_id, current_page=4)
response = client.post(f'/apply/{session_id}/submit-application', follow_redirects=False)
assert response.status_code == 302
assert b'/confirmation' in response.data
class TestPage5Confirmation:
"""Tests for Page 5: Confirmation."""
def test_page5_loads(self, client, create_test_application):
"""Test that confirmation page loads successfully."""
session_id = 'test-page5-123'
create_test_application(session_id=session_id, current_page=5, email='test@example.com')
response = client.get(f'/apply/{session_id}/confirmation')
assert response.status_code == 200
assert b'Vielen Dank' in response.data or b'erfolgreich' in response.data
class TestResumeFunction:
"""Tests for resume functionality."""
def test_resume_redirects_to_correct_page(self, client, create_test_application):
"""Test that resume redirects to the correct page based on current_page."""
# Test redirect to page 2
session_id = 'test-resume-page2'
create_test_application(session_id=session_id, current_page=2)
response = client.get(f'/resume/{session_id}', follow_redirects=False)
assert response.status_code == 302
assert b'/personal' in response.data
# Test redirect to page 3
session_id = 'test-resume-page3'
create_test_application(session_id=session_id, current_page=3)
response = client.get(f'/resume/{session_id}', follow_redirects=False)
assert response.status_code == 302
assert b'/motivation' in response.data
# Test redirect to page 4
session_id = 'test-resume-page4'
create_test_application(session_id=session_id, current_page=4)
response = client.get(f'/resume/{session_id}', follow_redirects=False)
assert response.status_code == 302
assert b'/upload' in response.data
def test_resume_nonexistent_session(self, client):
"""Test resuming with non-existent session ID."""
response = client.get('/resume/nonexistent-session', follow_redirects=True)
assert response.status_code == 200
class TestIndexRoute:
"""Tests for index route."""
def test_index_redirects_to_apply(self, client):
"""Test that index redirects to the apply page."""
response = client.get('/', follow_redirects=False)
assert response.status_code == 302
assert b'/apply' in response.data
+214
View File
@@ -0,0 +1,214 @@
"""
Tests for data storage (YAML files and file system operations).
"""
import pytest
import os
import yaml
from pathlib import Path
from app import (
save_application_data,
load_application_data,
get_application_path,
get_data_file_path,
get_attachments_path
)
class TestYAMLStorage:
"""Tests for YAML data storage."""
def test_save_application_data(self, app, sample_application_data):
"""Test saving application data to YAML file."""
session_id = sample_application_data['session_id']
save_application_data(session_id, sample_application_data)
# Verify file was created
data_file = get_data_file_path(session_id)
assert os.path.exists(data_file)
# Verify content
with open(data_file, 'r', encoding='utf-8') as f:
loaded_data = yaml.safe_load(f)
assert loaded_data['session_id'] == session_id
assert loaded_data['email'] == sample_application_data['email']
assert loaded_data['job_name'] == sample_application_data['job_name']
def test_load_application_data(self, app, sample_application_data):
"""Test loading application data from YAML file."""
session_id = sample_application_data['session_id']
# Save data first
save_application_data(session_id, sample_application_data)
# Load and verify
loaded_data = load_application_data(session_id)
assert loaded_data is not None
assert loaded_data['session_id'] == session_id
assert loaded_data['email'] == sample_application_data['email']
assert loaded_data['personal_info']['name'] == 'Müller'
def test_load_nonexistent_application(self, app):
"""Test loading data for non-existent application."""
loaded_data = load_application_data('nonexistent-session')
assert loaded_data is None
def test_unicode_characters_in_yaml(self, app):
"""Test that German umlauts and special characters are preserved."""
session_id = 'test-unicode-123'
data = {
'session_id': session_id,
'email': 'test@example.com',
'job_name': 'Test Job',
'current_page': 2,
'personal_info': {
'name': 'Müller',
'firstname': 'Jürgen',
'city': 'München',
'notes': 'Grüße aus Zürich'
}
}
save_application_data(session_id, data)
loaded_data = load_application_data(session_id)
assert loaded_data['personal_info']['name'] == 'Müller'
assert loaded_data['personal_info']['firstname'] == 'Jürgen'
assert loaded_data['personal_info']['city'] == 'München'
assert loaded_data['personal_info']['notes'] == 'Grüße aus Zürich'
def test_update_existing_application(self, app, sample_application_data):
"""Test updating existing application data."""
session_id = sample_application_data['session_id']
# Save initial data
save_application_data(session_id, sample_application_data)
# Update data
sample_application_data['current_page'] = 3
sample_application_data['motivation_answers'] = {
'current_job': 'Developer',
'motivation': 'Great company'
}
save_application_data(session_id, sample_application_data)
# Verify update
loaded_data = load_application_data(session_id)
assert loaded_data['current_page'] == 3
assert 'motivation_answers' in loaded_data
assert loaded_data['motivation_answers']['current_job'] == 'Developer'
class TestFolderStructure:
"""Tests for application folder structure."""
def test_application_folder_created(self, app, sample_application_data):
"""Test that application folder is created when saving data."""
session_id = sample_application_data['session_id']
save_application_data(session_id, sample_application_data)
app_path = get_application_path(session_id)
assert os.path.exists(app_path)
assert os.path.isdir(app_path)
def test_data_yaml_created(self, app, sample_application_data):
"""Test that data.yaml file is created."""
session_id = sample_application_data['session_id']
save_application_data(session_id, sample_application_data)
data_file = get_data_file_path(session_id)
assert os.path.exists(data_file)
assert os.path.isfile(data_file)
assert data_file.endswith('data.yaml')
def test_attachments_folder_path(self, app):
"""Test that attachments folder path is correct."""
session_id = 'test-attachments-123'
attachments_path = get_attachments_path(session_id)
assert 'attachments' in attachments_path
assert session_id in attachments_path
class TestDataPersistence:
"""Tests for data persistence across workflow."""
def test_data_persists_across_pages(self, app, client):
"""Test that data persists as user progresses through pages."""
# Submit email
response = client.post('/apply/submit-email', data={
'email': 'persist@example.com',
'job_name': 'Persistence Test'
}, follow_redirects=True)
# Extract session ID from response
# For testing, we'll create a known session
from app import save_application_data
session_id = 'test-persist-123'
save_application_data(session_id, {
'session_id': session_id,
'email': 'persist@example.com',
'job_name': 'Persistence Test',
'current_page': 2,
'personal_info': {},
'motivation_answers': {},
'uploaded_files': []
})
# Submit personal info
client.post(f'/apply/{session_id}/submit-personal', data={
'name': 'Test',
'firstname': 'User',
'address': 'Street 1',
'zip_code': '12345',
'city': 'City',
'phone': '+41 79 123 45 67',
'birth_year': '1990'
})
# Load and verify personal info was saved
loaded_data = load_application_data(session_id)
assert loaded_data is not None
assert loaded_data['personal_info']['name'] == 'Test'
assert loaded_data['personal_info']['firstname'] == 'User'
assert loaded_data['current_page'] == 3 # Should be updated
# Submit motivation
client.post(f'/apply/{session_id}/submit-motivation', data={
'current_job': 'Developer',
'motivation': 'Love coding',
'qualifications': 'Python expert',
'salary': '100000'
})
# Load and verify motivation was saved
loaded_data = load_application_data(session_id)
assert loaded_data['motivation_answers']['current_job'] == 'Developer'
assert loaded_data['motivation_answers']['motivation'] == 'Love coding'
assert loaded_data['current_page'] == 4
def test_empty_optional_fields_preserved(self, app):
"""Test that empty optional fields are preserved correctly."""
session_id = 'test-empty-fields'
data = {
'session_id': session_id,
'email': 'test@example.com',
'job_name': 'Test',
'current_page': 3,
'personal_info': {
'civil_status': '' # Optional, empty
},
'motivation_answers': {
'current_job': '', # Optional, empty
'motivation': 'Some text',
'qualifications': '',
'salary': ''
}
}
save_application_data(session_id, data)
loaded_data = load_application_data(session_id)
assert loaded_data['personal_info']['civil_status'] == ''
assert loaded_data['motivation_answers']['current_job'] == ''
assert loaded_data['motivation_answers']['motivation'] == 'Some text'
+232
View File
@@ -0,0 +1,232 @@
"""
Tests for file upload functionality.
"""
import pytest
import os
from io import BytesIO
from app import load_application_data, get_attachments_path
class TestFileUpload:
"""Tests for file upload functionality."""
def test_upload_valid_pdf(self, client, create_test_application, sample_pdf_file):
"""Test uploading a valid PDF file."""
session_id = 'test-upload-pdf'
create_test_application(session_id=session_id, current_page=4)
sample_pdf_file.seek(0)
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'resume.pdf')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
# Verify file was added to data
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 1
assert loaded_data['uploaded_files'][0]['original_name'] == 'resume.pdf'
def test_upload_valid_jpg(self, client, create_test_application, sample_jpg_file):
"""Test uploading a valid JPG image."""
session_id = 'test-upload-jpg'
create_test_application(session_id=session_id, current_page=4)
sample_jpg_file.seek(0)
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_jpg_file, 'photo.jpg')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 1
def test_upload_multiple_files(self, client, create_test_application, sample_pdf_file, sample_jpg_file):
"""Test uploading multiple files."""
session_id = 'test-upload-multiple'
create_test_application(session_id=session_id, current_page=4)
# Upload first file
sample_pdf_file.seek(0)
client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'file1.pdf')},
content_type='multipart/form-data'
)
# Upload second file
sample_jpg_file.seek(0)
client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_jpg_file, 'file2.jpg')},
content_type='multipart/form-data'
)
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 2
def test_upload_exceeds_max_files(self, client, create_test_application, sample_pdf_file):
"""Test that uploading more than 3 files is rejected."""
session_id = 'test-upload-max'
create_test_application(
session_id=session_id,
current_page=4,
uploaded_files=[
{'original_name': 'file1.pdf', 'stored_name': 'file1.pdf', 'size': 1000},
{'original_name': 'file2.pdf', 'stored_name': 'file2.pdf', 'size': 1000},
{'original_name': 'file3.pdf', 'stored_name': 'file3.pdf', 'size': 1000}
]
)
sample_pdf_file.seek(0)
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'file4.pdf')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
# Should still have 3 files (4th was rejected)
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 3
def test_upload_invalid_file_type(self, client, create_test_application):
"""Test that invalid file types are rejected."""
session_id = 'test-upload-invalid'
create_test_application(session_id=session_id, current_page=4)
invalid_file = BytesIO(b'executable content')
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (invalid_file, 'virus.exe')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
loaded_data = load_application_data(session_id)
assert len(loaded_data.get('uploaded_files', [])) == 0
def test_upload_file_too_large(self, client, create_test_application, large_file):
"""Test that files larger than 4MB are rejected."""
session_id = 'test-upload-large'
create_test_application(session_id=session_id, current_page=4)
large_file.seek(0)
response = client.post(
f'/apply/{session_id}/upload-file',
data={'file': (large_file, 'large.pdf')},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
loaded_data = load_application_data(session_id)
assert len(loaded_data.get('uploaded_files', [])) == 0
def test_upload_no_file_selected(self, client, create_test_application):
"""Test uploading without selecting a file."""
session_id = 'test-upload-nofile'
create_test_application(session_id=session_id, current_page=4)
response = client.post(
f'/apply/{session_id}/upload-file',
data={},
content_type='multipart/form-data',
follow_redirects=True
)
assert response.status_code == 200
class TestFileRemoval:
"""Tests for file removal functionality."""
def test_remove_uploaded_file(self, client, create_test_application, sample_pdf_file, app):
"""Test removing an uploaded file."""
session_id = 'test-remove-file'
create_test_application(session_id=session_id, current_page=4)
# Upload a file
sample_pdf_file.seek(0)
client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'remove_me.pdf')},
content_type='multipart/form-data'
)
# Verify it was uploaded
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 1
# Remove the file
response = client.post(
f'/apply/{session_id}/remove-file/0',
follow_redirects=True
)
assert response.status_code == 200
loaded_data = load_application_data(session_id)
assert len(loaded_data['uploaded_files']) == 0
def test_remove_nonexistent_file(self, client, create_test_application):
"""Test removing a file with invalid index."""
session_id = 'test-remove-invalid'
create_test_application(session_id=session_id, current_page=4)
response = client.post(
f'/apply/{session_id}/remove-file/999',
follow_redirects=True
)
assert response.status_code == 200
class TestFileStorage:
"""Tests for file storage on disk."""
def test_file_saved_to_attachments_folder(self, client, create_test_application, sample_pdf_file, app):
"""Test that uploaded files are saved to the attachments folder."""
session_id = 'test-file-storage'
create_test_application(session_id=session_id, current_page=4)
sample_pdf_file.seek(0)
client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, 'stored.pdf')},
content_type='multipart/form-data'
)
# Check that file exists in attachments folder
attachments_path = get_attachments_path(session_id)
assert os.path.exists(attachments_path)
# Check that a file was created
files = os.listdir(attachments_path)
assert len(files) > 0
def test_filename_sanitization(self, client, create_test_application, sample_pdf_file):
"""Test that filenames are sanitized for security."""
session_id = 'test-sanitize'
create_test_application(session_id=session_id, current_page=4)
sample_pdf_file.seek(0)
# Try uploading a file with potentially dangerous name
client.post(
f'/apply/{session_id}/upload-file',
data={'file': (sample_pdf_file, '../../../etc/passwd.pdf')},
content_type='multipart/form-data'
)
loaded_data = load_application_data(session_id)
if len(loaded_data['uploaded_files']) > 0:
# Filename should be sanitized
stored_name = loaded_data['uploaded_files'][0]['stored_name']
assert '../' not in stored_name
assert '/' not in stored_name.split('_')[-1] # After timestamp
+148
View File
@@ -0,0 +1,148 @@
"""
Tests for validation functions in the Flask job application system.
"""
import pytest
from app import validate_email, validate_phone, validate_year, validate_zip, allowed_file
class TestEmailValidation:
"""Tests for email validation."""
def test_valid_email(self):
"""Test that valid email addresses are accepted."""
assert validate_email('test@example.com') is True
assert validate_email('user.name@domain.co.uk') is True
assert validate_email('test+tag@example.com') is True
assert validate_email('test_123@test-domain.com') is True
def test_invalid_email(self):
"""Test that invalid email addresses are rejected."""
assert validate_email('') is False
assert validate_email('invalid-email') is False
assert validate_email('@example.com') is False
assert validate_email('test@') is False
assert validate_email('test@domain') is False
assert validate_email('test @example.com') is False
def test_email_edge_cases(self):
"""Test edge cases for email validation."""
assert validate_email('a@b.co') is True # Minimum valid email
assert validate_email('test@@example.com') is False # Double @
assert validate_email('test..name@example.com') is True # Double dot in local part
class TestPhoneValidation:
"""Tests for international phone number validation."""
def test_valid_phone_with_spaces(self):
"""Test valid phone numbers with spaces."""
assert validate_phone('+41 79 123 45 67') is True
assert validate_phone('+1 555 123 4567') is True
assert validate_phone('+49 30 12345678') is True
def test_valid_phone_without_spaces(self):
"""Test valid phone numbers without spaces."""
assert validate_phone('+41791234567') is True
assert validate_phone('+15551234567') is True
assert validate_phone('+493012345678') is True
def test_invalid_phone(self):
"""Test that invalid phone numbers are rejected."""
assert validate_phone('') is False
assert validate_phone('079 123 45 67') is False # Missing +
assert validate_phone('+41') is False # Too short
assert validate_phone('41791234567') is False # Missing +
assert validate_phone('+') is False # Only +
def test_phone_edge_cases(self):
"""Test edge cases for phone validation."""
assert validate_phone('+1 123') is True # Minimum length
assert validate_phone('+999 1234567890123456') is True # Long number within limit
class TestYearValidation:
"""Tests for birth year validation."""
def test_valid_years(self):
"""Test that valid birth years are accepted."""
assert validate_year('1940') is True # Minimum
assert validate_year('1990') is True # Middle
assert validate_year('2010') is True # Maximum
def test_invalid_years_out_of_range(self):
"""Test that years outside the valid range are rejected."""
assert validate_year('1939') is False # Too old
assert validate_year('2011') is False # Too young
assert validate_year('1900') is False
assert validate_year('2025') is False
def test_invalid_year_format(self):
"""Test that invalid year formats are rejected."""
assert validate_year('') is False
assert validate_year('90') is False # 2 digits
assert validate_year('990') is False # 3 digits
assert validate_year('19900') is False # 5 digits
assert validate_year('abcd') is False # Non-numeric
assert validate_year('199a') is False # Mixed
class TestZipValidation:
"""Tests for ZIP code validation."""
def test_valid_zip_codes(self):
"""Test that valid ZIP codes are accepted."""
assert validate_zip('8001') is True
assert validate_zip('12345') is True
assert validate_zip('1') is True # Single digit
assert validate_zip('1234567890') is True # Max 10 digits
def test_invalid_zip_codes(self):
"""Test that invalid ZIP codes are rejected."""
assert validate_zip('') is False
assert validate_zip('12345678901') is False # 11 digits (too long)
assert validate_zip('abc') is False # Non-numeric
assert validate_zip('123a5') is False # Mixed
def test_zip_edge_cases(self):
"""Test edge cases for ZIP validation."""
assert validate_zip('0') is True # Zero is valid
assert validate_zip('00000') is True # Leading zeros
class TestFileExtensionValidation:
"""Tests for file extension validation."""
def test_valid_file_extensions(self):
"""Test that files with valid extensions are accepted."""
assert allowed_file('document.pdf') is True
assert allowed_file('resume.doc') is True
assert allowed_file('cover.docx') is True
assert allowed_file('notes.txt') is True
assert allowed_file('photo.jpg') is True
assert allowed_file('image.jpeg') is True
assert allowed_file('picture.png') is True
def test_invalid_file_extensions(self):
"""Test that files with invalid extensions are rejected."""
assert allowed_file('script.exe') is False
assert allowed_file('data.zip') is False
assert allowed_file('code.py') is False
assert allowed_file('file.unknown') is False
def test_file_without_extension(self):
"""Test that files without extensions are rejected."""
assert allowed_file('noextension') is False
assert allowed_file('') is False
def test_case_insensitive_extensions(self):
"""Test that file extension validation is case-insensitive."""
assert allowed_file('document.PDF') is True
assert allowed_file('document.Pdf') is True
assert allowed_file('image.JPG') is True
assert allowed_file('image.JPEG') is True
def test_multiple_dots_in_filename(self):
"""Test files with multiple dots in the filename."""
assert allowed_file('my.document.pdf') is True
assert allowed_file('file.name.with.dots.jpg') is True
assert allowed_file('test.tar.gz') is False # .gz not allowed