Tests added
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
@@ -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
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 333 B |
Vendored
+12
@@ -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
|
||||
@@ -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('.')
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user