refactor: split monolithic app.py into modular Flask application with factory pattern

- Created app/ package with 6 modules:
  - __init__.py: Application factory (create_app function)
  - routes.py: All route handlers (473 lines)
  - models.py: Data persistence layer (41 lines)
  - validators.py: Input validation functions (48 lines)
  - email_service.py: Email sending functions (85 lines)
  - utils.py: Utility functions (30 lines)

- Implemented Flask application factory pattern
- Created run.py as minimal entry point
- Updated all test imports to use new module structure
- Fixed template/static folder paths for package structure

- All 102 tests passing
- Improved maintainability with clear separation of concerns
- Follows Flask best practices for scalable applications
This commit is contained in:
2025-12-27 22:36:05 +01:00
parent 733ee756c7
commit f1d8bae6a1
18 changed files with 1254 additions and 229 deletions
+8 -3
View File
@@ -8,7 +8,7 @@ from io import BytesIO
from datetime import datetime
import pytest
import yaml
from app import app as flask_app
from app import create_app
from flask_mail import Mail
@@ -18,6 +18,9 @@ def app():
# Create a temporary directory for test applications
temp_dir = tempfile.mkdtemp()
# Create test Flask app using factory
flask_app = create_app()
# Configure app for testing
flask_app.config['TESTING'] = True
flask_app.config['SECRET_KEY'] = 'test-secret-key'
@@ -133,7 +136,9 @@ def mock_mail(app, monkeypatch):
self.sent_messages.append(message)
mock = MockMail()
monkeypatch.setattr('app.mail', mock)
# Patch the mail instance in the app package (used by email_service)
import app.email_service
monkeypatch.setattr(app.email_service, 'mail', mock)
return mock
@@ -142,7 +147,7 @@ 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
from app.models import save_application_data
data = {
'session_id': session_id,
+4 -4
View File
@@ -174,7 +174,7 @@ class TestFileDownload:
session_id = 'test-download-success'
# Create application directory and attachment
from app import get_attachments_path
from app.models import get_attachments_path
attachments_path = get_attachments_path(session_id)
Path(attachments_path).mkdir(parents=True, exist_ok=True)
@@ -215,7 +215,7 @@ class TestFileDownload:
session_id = 'test-download-unauthorized'
# Create application directory and attachment
from app import get_attachments_path
from app.models import get_attachments_path
attachments_path = get_attachments_path(session_id)
Path(attachments_path).mkdir(parents=True, exist_ok=True)
@@ -273,7 +273,7 @@ class TestFileDownload:
session_id = 'test-download-multiple'
# Create application directory
from app import get_attachments_path
from app.models import get_attachments_path
attachments_path = get_attachments_path(session_id)
Path(attachments_path).mkdir(parents=True, exist_ok=True)
@@ -318,7 +318,7 @@ class TestFileDownload:
session_id = 'test-download-special-chars'
# Create application directory
from app import get_attachments_path
from app.models import get_attachments_path
attachments_path = get_attachments_path(session_id)
Path(attachments_path).mkdir(parents=True, exist_ok=True)
+26 -22
View File
@@ -4,7 +4,7 @@ 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
from app.models import get_application_path, get_data_file_path, get_attachments_path
class TestConfiguration:
@@ -59,33 +59,37 @@ class TestPathHelpers:
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
with app.app_context():
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
with app.app_context():
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
with app.app_context():
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('.')
with app.app_context():
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('.')
+34 -30
View File
@@ -4,7 +4,7 @@ Tests for email functionality.
import pytest
from unittest.mock import patch, MagicMock
from flask_mail import Message
from app import send_resume_email
from app.email_service import send_resume_email
class TestEmailSending:
@@ -12,47 +12,51 @@ class TestEmailSending:
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')
with app.app_context():
with patch('app.email_service.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
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')
with app.app_context():
with patch('app.email_service.mail.send') as mock_send:
mock_send.side_effect = Exception('SMTP error')
result = send_resume_email('test@example.com', 'test-session-123', 'Test Job')
result = send_resume_email('test@example.com', 'test-session-123', 'Test Job')
assert result is False
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')
with app.app_context():
with patch('app.email_service.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:
# 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 and hasattr(message, 'body'):
assert 'session-xyz' in message.body or '/resume/' in message.body
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 app.app_context():
with patch('app.email_service.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:
@@ -60,7 +64,7 @@ class TestEmailIntegration:
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:
with patch('app.email_service.mail.send') as mock_send:
response = client.post('/apply/submit-email', data={
'email': 'newuser@example.com',
'job_name': 'Junior Developer'
+8 -17
View File
@@ -3,16 +3,15 @@ Tests for HR email notifications functionality.
"""
import pytest
from unittest.mock import patch, MagicMock
from app import send_hr_notification
from app.email_service import send_hr_notification
class TestHRNotifications:
"""Test HR notification email functionality."""
def test_hr_notification_sent_on_submission(self, client, create_test_application, mock_mail):
def test_hr_notification_sent_on_submission(self, app, client, create_test_application, mock_mail):
"""Test that HR receives email when application is submitted."""
# Set HR_EMAIL in config
from app import app
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
@@ -59,11 +58,10 @@ class TestHRNotifications:
assert f'/application/{session_id}/' in hr_email.body
def test_hr_notification_not_sent_when_hr_email_not_configured(
self, client, create_test_application, mock_mail, caplog
self, app, client, create_test_application, mock_mail, caplog
):
"""Test that HR notification is skipped when HR_EMAIL is not configured."""
# Ensure HR_EMAIL is not set
from app import app
app.config['HR_EMAIL'] = None
# Create a test application
@@ -97,9 +95,8 @@ class TestHRNotifications:
# Verify warning was logged
assert 'HR_EMAIL not configured' in caplog.text
def test_hr_notification_includes_file_count(self, client, create_test_application, mock_mail):
def test_hr_notification_includes_file_count(self, app, client, create_test_application, mock_mail):
"""Test that HR notification includes the count of uploaded files."""
from app import app
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
@@ -146,9 +143,8 @@ class TestHRNotifications:
hr_email = mock_mail.sent_messages[0]
assert 'Anzahl der hochgeladenen Dokumente: 2' in hr_email.body
def test_hr_notification_with_zero_files(self, client, create_test_application, mock_mail):
def test_hr_notification_with_zero_files(self, app, client, create_test_application, mock_mail):
"""Test that HR notification works even when no files are uploaded."""
from app import app
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
@@ -184,8 +180,6 @@ class TestHRNotifications:
def test_send_hr_notification_function_directly(self, app):
"""Test the send_hr_notification function directly."""
from app import send_hr_notification
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
@@ -205,15 +199,13 @@ class TestHRNotifications:
with app.app_context():
# Mock mail.send to prevent actual sending
with patch('app.mail.send') as mock_send:
with patch('app.email_service.mail.send') as mock_send:
result = send_hr_notification(session_id, app_data)
assert result is True
assert mock_send.called
def test_send_hr_notification_handles_email_failure(self, app, caplog):
"""Test that send_hr_notification handles email sending failures gracefully."""
from app import send_hr_notification
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'http://localhost:5000'
@@ -231,14 +223,13 @@ class TestHRNotifications:
with app.app_context():
# Mock mail.send to raise an exception
with patch('app.mail.send', side_effect=Exception('SMTP error')):
with patch('app.email_service.mail.send', side_effect=Exception('SMTP error')):
result = send_hr_notification(session_id, app_data)
assert result is False
assert 'Failed to send HR notification email' in caplog.text
def test_hr_notification_url_format(self, client, create_test_application, mock_mail):
def test_hr_notification_url_format(self, app, client, create_test_application, mock_mail):
"""Test that the application URL in HR notification is correctly formatted."""
from app import app
app.config['HR_EMAIL'] = 'hr@example.com'
app.config['APPLICATION_URL_BASE'] = 'https://example.com'
+7 -7
View File
@@ -3,7 +3,7 @@ Integration tests for the complete application workflow.
"""
import pytest
from unittest.mock import patch
from app import load_application_data
from app.models import load_application_data
@pytest.mark.integration
@@ -13,7 +13,7 @@ class TestCompleteWorkflow:
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'):
with patch('app.email_service.mail.send'):
response = client.post('/apply/submit-email', data={
'email': 'integration@test.com',
'job_name': 'Integration Test Position'
@@ -22,7 +22,7 @@ class TestCompleteWorkflow:
assert response.status_code == 200
# For testing, create a known session
from app import save_application_data
from app.models import save_application_data
session_id = 'integration-test-123'
save_application_data(session_id, {
'session_id': session_id,
@@ -130,7 +130,7 @@ class TestCompleteWorkflow:
def test_multiple_concurrent_applications(self, client, app):
"""Test handling multiple concurrent applications with different sessions."""
from app import save_application_data
from app.models import save_application_data
# Create multiple applications
sessions = []
@@ -182,7 +182,7 @@ class TestWorkflowEdgeCases:
def test_skip_optional_fields(self, client, app):
"""Test completing workflow without filling optional fields."""
from app import save_application_data
from app.models import save_application_data
session_id = 'optional-test'
save_application_data(session_id, {
@@ -226,7 +226,7 @@ class TestWorkflowEdgeCases:
def test_workflow_with_unicode_data(self, client, app):
"""Test workflow with German special characters."""
from app import save_application_data
from app.models import save_application_data
session_id = 'unicode-test'
save_application_data(session_id, {
@@ -265,7 +265,7 @@ class TestDataIntegrity:
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
from app.models import save_application_data
session_id = 'persistence-test'
save_application_data(session_id, {
+1 -1
View File
@@ -3,7 +3,7 @@ Tests for routes and form submissions in the Flask job application system.
"""
import pytest
from flask import session
from app import load_application_data
from app.models import load_application_data
class TestPage1Email:
+101 -92
View File
@@ -5,7 +5,7 @@ import pytest
import os
import yaml
from pathlib import Path
from app import (
from app.models import (
save_application_data,
load_application_data,
get_application_path,
@@ -19,84 +19,89 @@ class TestYAMLStorage:
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)
with app.app_context():
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 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)
# 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']
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']
with app.app_context():
session_id = sample_application_data['session_id']
# Save data first
save_application_data(session_id, sample_application_data)
# 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'
# 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
with app.app_context():
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'
with app.app_context():
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)
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'
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']
with app.app_context():
session_id = sample_application_data['session_id']
# Save initial data
save_application_data(session_id, sample_application_data)
# 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)
# 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'
# 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:
@@ -104,30 +109,33 @@ class TestFolderStructure:
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)
with app.app_context():
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)
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)
with app.app_context():
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')
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)
with app.app_context():
session_id = 'test-attachments-123'
attachments_path = get_attachments_path(session_id)
assert 'attachments' in attachments_path
assert session_id in attachments_path
assert 'attachments' in attachments_path
assert session_id in attachments_path
class TestDataPersistence:
@@ -143,7 +151,7 @@ class TestDataPersistence:
# Extract session ID from response
# For testing, we'll create a known session
from app import save_application_data
from app.models import save_application_data
session_id = 'test-persist-123'
save_application_data(session_id, {
'session_id': session_id,
@@ -189,26 +197,27 @@ class TestDataPersistence:
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': ''
with app.app_context():
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)
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'
assert loaded_data['personal_info']['civil_status'] == ''
assert loaded_data['motivation_answers']['current_job'] == ''
assert loaded_data['motivation_answers']['motivation'] == 'Some text'
+1 -1
View File
@@ -4,7 +4,7 @@ Tests for file upload functionality.
import pytest
import os
from io import BytesIO
from app import load_application_data, get_attachments_path
from app.models import load_application_data, get_attachments_path
class TestFileUpload:
+62 -52
View File
@@ -2,7 +2,7 @@
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
from app.validators import validate_email, validate_phone, validate_year, validate_zip, allowed_file
class TestEmailValidation:
@@ -63,86 +63,96 @@ class TestPhoneValidation:
class TestYearValidation:
"""Tests for birth year validation."""
def test_valid_years(self):
def test_valid_years(self, app):
"""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
with app.app_context():
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):
def test_invalid_years_out_of_range(self, app):
"""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
with app.app_context():
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):
def test_invalid_year_format(self, app):
"""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
with app.app_context():
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):
def test_valid_zip_codes(self, app):
"""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
with app.app_context():
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):
def test_invalid_zip_codes(self, app):
"""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
with app.app_context():
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):
def test_zip_edge_cases(self, app):
"""Test edge cases for ZIP validation."""
assert validate_zip('0') is True # Zero is valid
assert validate_zip('00000') is True # Leading zeros
with app.app_context():
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):
def test_valid_file_extensions(self, app):
"""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
with app.app_context():
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):
def test_invalid_file_extensions(self, app):
"""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
with app.app_context():
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):
def test_case_insensitive_extensions(self, app):
"""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
with app.app_context():
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):
def test_multiple_dots_in_filename(self, app):
"""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
with app.app_context():
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