""" Tests for email functionality """ import pytest pytestmark = [pytest.mark.unit, pytest.mark.utils] 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="

Test body

", ) # 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