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:
+8
-3
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user