Files
TimeTracker/tests/test_email.py
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

312 lines
12 KiB
Python

"""
Tests for email functionality
"""
import pytest
from unittest.mock import patch, MagicMock
from flask import current_app
from app.utils.email import send_email, check_email_configuration, send_test_email, init_mail
class TestEmailConfiguration:
"""Tests for email configuration"""
def test_init_mail(self, app):
"""Test email initialization"""
with app.app_context():
mail = init_mail(app)
assert mail is not None
assert "MAIL_SERVER" in app.config
assert "MAIL_PORT" in app.config
assert "MAIL_DEFAULT_SENDER" in app.config
def test_email_config_status_not_configured(self, app):
"""Test email configuration status when not configured"""
with app.app_context():
# Reset mail server to simulate unconfigured state
app.config["MAIL_SERVER"] = "localhost"
status = check_email_configuration()
assert status is not None
assert "configured" in status
assert "settings" in status
assert "errors" in status
assert "warnings" in status
assert status["configured"] is False
assert len(status["errors"]) > 0
def test_email_config_status_configured(self, app):
"""Test email configuration status when properly configured"""
with app.app_context():
# Set up proper configuration
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_PORT"] = 587
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USE_SSL"] = False
app.config["MAIL_USERNAME"] = "test@example.com"
app.config["MAIL_PASSWORD"] = "test_password"
app.config["MAIL_DEFAULT_SENDER"] = "noreply@example.com"
status = check_email_configuration()
assert status is not None
assert status["configured"] is True
assert len(status["errors"]) == 0
assert status["settings"]["server"] == "smtp.gmail.com"
assert status["settings"]["port"] == 587
assert status["settings"]["password_set"] is True
def test_email_config_warns_about_default_sender(self, app):
"""Test that configuration warns about default sender"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_DEFAULT_SENDER"] = "noreply@timetracker.local"
status = check_email_configuration()
assert len(status["warnings"]) > 0
assert any("Default sender" in w for w in status["warnings"])
def test_email_config_errors_on_both_tls_and_ssl(self, app):
"""Test that configuration errors when both TLS and SSL are enabled"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USE_SSL"] = True
status = check_email_configuration()
assert len(status["errors"]) > 0
assert any("TLS and SSL" in e for e in status["errors"])
class TestSendEmail:
"""Tests for sending emails"""
@patch("app.utils.email.mail.send")
@patch("app.utils.email.Thread")
def test_send_email_success(self, mock_thread, mock_send, app):
"""Test sending email successfully"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
send_email(
subject="Test Subject",
recipients=["test@example.com"],
text_body="Test body",
html_body="<p>Test body</p>",
)
# Verify thread was started for async sending
assert mock_thread.called
def test_send_email_no_server(self, app):
"""Test sending email with no mail server configured"""
with app.app_context():
app.config["MAIL_SERVER"] = None
# This should not raise an exception, but log a warning and return early
send_email(subject="Test Subject", recipients=["test@example.com"], text_body="Test body")
# The function should return without error
# (The warning is logged but we can't easily capture it in this context)
def test_send_email_no_recipients(self, app):
"""Test sending email with no recipients"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
# This should not raise an exception, but log a warning and return early
send_email(subject="Test Subject", recipients=[], text_body="Test body")
# The function should return without error
# (The warning is logged but we can't easily capture it in this context)
@patch("app.utils.email.mail.send")
def test_send_test_email_success(self, mock_send, app):
"""Test sending test email successfully"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_DEFAULT_SENDER"] = "test@example.com"
success, message = send_test_email("recipient@example.com", "Test Sender")
assert success is True
assert "successfully" in message.lower()
assert mock_send.called
def test_send_test_email_invalid_recipient(self, app):
"""Test sending test email with invalid recipient"""
with app.app_context():
success, message = send_test_email("invalid-email", "Test Sender")
assert success is False
assert "Invalid" in message
def test_send_test_email_no_server(self, app):
"""Test sending test email with no mail server"""
with app.app_context():
app.config["MAIL_SERVER"] = None
success, message = send_test_email("test@example.com", "Test Sender")
assert success is False
assert "not configured" in message
@patch("app.utils.email.mail.send")
def test_send_test_email_exception(self, mock_send, app):
"""Test sending test email with exception"""
with app.app_context():
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_DEFAULT_SENDER"] = "test@example.com"
# Simulate exception
mock_send.side_effect = Exception("SMTP error")
success, message = send_test_email("test@example.com", "Test Sender")
assert success is False
assert "Failed" in message
class TestEmailIntegration:
"""Integration tests for email functionality"""
def check_email_configuration_in_app_context(self, app):
"""Test that email configuration is available in app context"""
with app.app_context():
assert hasattr(current_app, "config")
assert "MAIL_SERVER" in current_app.config
assert "MAIL_PORT" in current_app.config
assert "MAIL_USE_TLS" in current_app.config
assert "MAIL_DEFAULT_SENDER" in current_app.config
def test_email_settings_from_environment(self, app, monkeypatch):
"""Test that email settings are loaded from environment"""
# Set environment variables
monkeypatch.setenv("MAIL_SERVER", "smtp.test.com")
monkeypatch.setenv("MAIL_PORT", "465")
monkeypatch.setenv("MAIL_USE_SSL", "true")
# Reinitialize mail with new environment
with app.app_context():
mail = init_mail(app)
assert app.config["MAIL_SERVER"] == "smtp.test.com"
assert app.config["MAIL_PORT"] == 465
assert app.config["MAIL_USE_SSL"] is True
class TestDatabaseEmailConfiguration:
"""Tests for database-backed email configuration"""
def test_get_mail_config_when_disabled(self, app):
"""Test get_mail_config returns None when database config is disabled"""
with app.app_context():
from app.models import Settings
settings = Settings.get_settings()
settings.mail_enabled = False
settings.mail_server = "smtp.test.com"
config = settings.get_mail_config()
assert config is None
def test_get_mail_config_when_enabled(self, app):
"""Test get_mail_config returns config when enabled"""
with app.app_context():
from app.models import Settings
settings = Settings.get_settings()
settings.mail_enabled = True
settings.mail_server = "smtp.test.com"
settings.mail_port = 587
settings.mail_use_tls = True
settings.mail_use_ssl = False
settings.mail_username = "test@example.com"
settings.mail_password = "test_password"
settings.mail_default_sender = "noreply@example.com"
config = settings.get_mail_config()
assert config is not None
assert config["MAIL_SERVER"] == "smtp.test.com"
assert config["MAIL_PORT"] == 587
assert config["MAIL_USE_TLS"] is True
assert config["MAIL_USE_SSL"] is False
assert config["MAIL_USERNAME"] == "test@example.com"
assert config["MAIL_PASSWORD"] == "test_password"
assert config["MAIL_DEFAULT_SENDER"] == "noreply@example.com"
def test_init_mail_uses_database_config(self, app):
"""Test that init_mail uses database settings when available"""
with app.app_context():
from app.models import Settings
from app.utils.email import init_mail
from app import db
settings = Settings.get_settings()
settings.mail_enabled = True
settings.mail_server = "smtp.database.com"
settings.mail_port = 465
db.session.commit()
init_mail(app)
# Should use database settings
assert app.config["MAIL_SERVER"] == "smtp.database.com"
assert app.config["MAIL_PORT"] == 465
def test_reload_mail_config(self, app):
"""Test reloading email configuration"""
with app.app_context():
from app.models import Settings
from app.utils.email import reload_mail_config
from app import db
# Set up database config
settings = Settings.get_settings()
settings.mail_enabled = True
settings.mail_server = "smtp.reloaded.com"
db.session.commit()
# Reload configuration
success = reload_mail_config(app)
assert success is True
assert app.config["MAIL_SERVER"] == "smtp.reloaded.com"
def test_check_email_configuration_shows_source(self, app):
"""Test that configuration status shows source (database or environment)"""
with app.app_context():
from app.models import Settings
from app.utils.email import check_email_configuration
from app import db
# Test with database config
settings = Settings.get_settings()
settings.mail_enabled = True
settings.mail_server = "smtp.database.com"
db.session.commit()
status = check_email_configuration()
assert "source" in status
assert status["source"] == "database"
# Test with environment config
settings.mail_enabled = False
db.session.commit()
status = check_email_configuration()
assert status["source"] == "environment"
# Fixtures
@pytest.fixture
def mock_mail_send():
"""Mock the mail.send method"""
with patch("app.utils.email.mail.send") as mock:
yield mock