import sys from pathlib import Path # Add api directory to path api_dir = Path(__file__).parent.parent / 'api' sys.path.insert(0, str(api_dir)) import unittest import tempfile import shutil import os from unittest.mock import patch, MagicMock from email_manager import EmailManager class TestEmailManager(unittest.TestCase): def setUp(self): self.test_dir = tempfile.mkdtemp() self.data_dir = os.path.join(self.test_dir, 'data') self.config_dir = os.path.join(self.test_dir, 'config') os.makedirs(self.data_dir, exist_ok=True) os.makedirs(self.config_dir, exist_ok=True) self.manager = EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) def tearDown(self): shutil.rmtree(self.test_dir) def test_initialization(self): self.assertTrue(os.path.exists(self.manager.email_dir)) self.assertTrue(os.path.exists(self.manager.postfix_dir)) self.assertTrue(os.path.exists(self.manager.dovecot_dir)) @patch.object(EmailManager, '_reload_email_services', return_value=True) def test_create_and_delete_email_user(self, mock_reload): result = self.manager.create_email_user('testuser', 'testdomain', 'password') self.assertTrue(result) result = self.manager.delete_email_user('testuser', 'testdomain') self.assertTrue(result) def test_list_email_users_empty(self): users = self.manager.list_email_users() self.assertIsInstance(users, list) self.assertEqual(len(users), 0) @patch('smtplib.SMTP') def test_send_email(self, mock_smtp): instance = mock_smtp.return_value.__enter__.return_value instance.sendmail.return_value = {} result = self.manager.send_email('from@cell', 'to@cell', 'Subject', 'Body') self.assertTrue(result) # Simulate error by raising in SMTP constructor mock_smtp.side_effect = Exception('SMTP error') result = self.manager.send_email('from@cell', 'to@cell', 'Subject', 'Body') self.assertFalse(result) @patch('imaplib.IMAP4_SSL') def test_get_mailbox_info(self, mock_imap): instance = mock_imap.return_value instance.login.return_value = 'OK' instance.select.return_value = ('OK', [b'1']) instance.search.return_value = ('OK', [b'1 2 3']) instance.fetch.return_value = ('OK', [(b'1', b'RFC822')]) info = self.manager.get_mailbox_info('testuser', 'testdomain') self.assertIsInstance(info, dict) instance.login.side_effect = Exception('IMAP error') info = self.manager.get_mailbox_info('testuser', 'testdomain') self.assertIn('error', info) @patch('subprocess.run') def test_get_email_status(self, mock_run): mock_run.return_value.stdout = 'cell-mail\n' mock_run.return_value.returncode = 0 status = self.manager.get_email_status() self.assertIsInstance(status, dict) self.assertIn('postfix_running', status) self.assertIn('dovecot_running', status) @patch('requests.get') def test_test_email_connectivity(self, mock_get): mock_get.return_value.status_code = 200 result = self.manager.test_email_connectivity() self.assertIsInstance(result, dict) mock_get.side_effect = Exception('HTTP error') result = self.manager.test_email_connectivity() self.assertIn('smtp', result) @patch('subprocess.run') def test_get_email_logs(self, mock_run): mock_run.return_value.stdout = 'log line\n' mock_run.return_value.returncode = 0 logs = self.manager.get_email_logs('all', 10) self.assertIsInstance(logs, dict) self.assertIn('postfix', logs) self.assertIn('dovecot', logs) def test_get_status(self): status = self.manager.get_status() self.assertIsInstance(status, dict) self.assertIn('status', status) def test_error_handling(self): # Force errors by passing invalid arguments, should return False or error dict self.assertFalse(self.manager.create_email_user(None, None, None)) self.assertFalse(self.manager.delete_email_user(None, None)) self.assertFalse(self.manager.send_email(None, None, None, None)) info = self.manager.get_mailbox_info(None, None) self.assertIn('error', info) class TestEmailManagerEffectiveDomain(unittest.TestCase): """Verify that email OVERRIDE_HOSTNAME and POSTMASTER_ADDRESS use the caller-supplied domain (which should come from get_effective_domain in the route layer when no explicit domain is provided by the client).""" def setUp(self): self.test_dir = tempfile.mkdtemp() self.data_dir = os.path.join(self.test_dir, 'data') self.config_dir = os.path.join(self.test_dir, 'config') os.makedirs(os.path.join(self.config_dir, 'mail'), exist_ok=True) os.makedirs(os.path.join(self.data_dir, 'email'), exist_ok=True) with open(os.path.join(self.config_dir, 'mail', 'mailserver.env'), 'w') as f: f.write('OVERRIDE_HOSTNAME=mail.cell\nPOSTMASTER_ADDRESS=admin@cell\n') sys.path.insert(0, str(Path(__file__).parent.parent / 'api')) from email_manager import EmailManager self.em = EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) def tearDown(self): shutil.rmtree(self.test_dir) @patch('subprocess.run') def test_email_hostname_uses_effective_domain_in_ddns_mode(self, mock_run): """When apply_config is called with domain='home.pic.ngo' (as provided by the route layer via get_effective_domain), OVERRIDE_HOSTNAME and POSTMASTER_ADDRESS should use 'home.pic.ngo', not the internal 'cell'.""" mock_run.return_value = MagicMock(returncode=0) result = self.em.apply_config({'domain': 'home.pic.ngo'}) env = open(os.path.join(self.config_dir, 'mail', 'mailserver.env')).read() self.assertIn('OVERRIDE_HOSTNAME=mail.home.pic.ngo', env) self.assertIn('POSTMASTER_ADDRESS=admin@home.pic.ngo', env) self.assertIn('cell-mail', result['restarted']) class TestEmailManagerIdentityChangedSubscription(unittest.TestCase): def setUp(self): self.test_dir = tempfile.mkdtemp() self.data_dir = os.path.join(self.test_dir, 'data') self.config_dir = os.path.join(self.test_dir, 'config') os.makedirs(self.data_dir, exist_ok=True) os.makedirs(self.config_dir, exist_ok=True) def tearDown(self): shutil.rmtree(self.test_dir) def test_subscribes_to_identity_changed_on_init(self): """When service_bus is provided, __init__ subscribes to IDENTITY_CHANGED.""" from service_bus import EventType mock_bus = MagicMock() manager = EmailManager( data_dir=self.data_dir, config_dir=self.config_dir, service_bus=mock_bus, ) mock_bus.subscribe_to_event.assert_called_once_with( EventType.IDENTITY_CHANGED, manager._on_identity_changed ) def test_no_subscription_without_service_bus(self): """When service_bus is not provided, no subscription is attempted.""" mock_bus = MagicMock() EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) mock_bus.subscribe_to_event.assert_not_called() @patch.object(EmailManager, 'apply_config', return_value={'restarted': [], 'warnings': []}) def test_on_identity_changed_calls_apply_config(self, mock_apply): """_on_identity_changed calls apply_config with the effective_domain.""" manager = EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) event = MagicMock() event.data = {'effective_domain': 'mycell.pic.ngo'} manager._on_identity_changed(event) mock_apply.assert_called_once_with({'domain': 'mycell.pic.ngo'}) @patch.object(EmailManager, 'apply_config', side_effect=Exception('boom')) def test_on_identity_changed_swallows_exceptions(self, mock_apply): """_on_identity_changed must not propagate exceptions.""" manager = EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) event = MagicMock() event.data = {'effective_domain': 'mycell.pic.ngo'} manager._on_identity_changed(event) # must not raise def test_on_identity_changed_skips_when_no_effective_domain(self): """_on_identity_changed does nothing when effective_domain is absent.""" manager = EmailManager(data_dir=self.data_dir, config_dir=self.config_dir) event = MagicMock() event.data = {'cell_name': 'mycell'} with patch.object(manager, 'apply_config') as mock_apply: manager._on_identity_changed(event) mock_apply.assert_not_called() if __name__ == '__main__': unittest.main()