#!/usr/bin/env python3 """ Additional tests for enhanced_cli.py covering uncovered paths: - EnhancedCLI.do_* methods - EnhancedCLI._display_* methods - EnhancedCLI.show_status, list_services, show_config - EnhancedCLI.batch_start_services, batch_stop_services - APIClient.request (PUT, DELETE branches, error handling) - Module-level: batch_operations, export_config, import_config """ import sys import json import tempfile import os import shutil import unittest from io import StringIO from pathlib import Path from unittest.mock import patch, MagicMock, call api_dir = Path(__file__).parent.parent / 'api' sys.path.insert(0, str(api_dir)) from enhanced_cli import EnhancedCLI, APIClient, ConfigManager, batch_operations, export_config, import_config class TestAPIClientExtra(unittest.TestCase): def setUp(self): self.client = APIClient('http://localhost:3000/api') def test_request_put_calls_session_put(self): mock_resp = MagicMock() mock_resp.json.return_value = {'ok': True} mock_resp.raise_for_status.return_value = None with patch.object(self.client.session, 'put', return_value=mock_resp) as mock_put: result = self.client.request('PUT', '/config', {'key': 'val'}) mock_put.assert_called_once() self.assertEqual(result, {'ok': True}) def test_request_delete_calls_session_delete(self): mock_resp = MagicMock() mock_resp.json.return_value = {'deleted': True} mock_resp.raise_for_status.return_value = None with patch.object(self.client.session, 'delete', return_value=mock_resp) as mock_del: result = self.client.request('DELETE', '/peers/alice') mock_del.assert_called_once() self.assertEqual(result, {'deleted': True}) def test_request_exception_returns_none(self): import requests as _req with patch.object(self.client.session, 'get', side_effect=_req.exceptions.RequestException('timeout')): result = self.client.request('GET', '/status') self.assertIsNone(result) class TestEnhancedCLIDoMethods(unittest.TestCase): def setUp(self): self.cli = EnhancedCLI.__new__(EnhancedCLI) self.cli.api_client = MagicMock() self.cli.config_manager = MagicMock() self.cli.current_service = None self.cli.prompt = 'picell> ' # ── do_status ───────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_status_prints_status(self, mock_print): self.cli.api_client.request.return_value = {'cell_name': 'mycel', 'peers_count': 2} self.cli.do_status('') self.assertTrue(any('mycel' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_status_prints_error_when_api_fails(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_status('') mock_print.assert_any_call('❌ Failed to get status') # ── do_services ─────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_services_prints_services(self, mock_print): self.cli.api_client.request.return_value = { 'email': {'running': True, 'status': 'online'}} self.cli.do_services('') self.assertTrue(any('email' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_services_prints_error_on_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_services('') mock_print.assert_any_call('❌ Failed to get services status') # ── do_peers ────────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_peers_empty_list_prints_message(self, mock_print): self.cli.api_client.request.return_value = [] self.cli.do_peers('') mock_print.assert_any_call('📭 No peers configured.') @patch('builtins.print') def test_do_peers_error_when_none_returned(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_peers('') mock_print.assert_any_call('❌ Failed to fetch peers') @patch('builtins.print') def test_do_peers_shows_peer_list(self, mock_print): self.cli.api_client.request.return_value = [ {'name': 'alice', 'ip': '10.0.0.2', 'public_key': 'abc123xyz', 'added_at': '2026-01-01'} ] self.cli.do_peers('') self.assertTrue(any('alice' in str(c) for c in mock_print.call_args_list)) # ── do_add_peer ─────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_add_peer_too_few_args(self, mock_print): self.cli.do_add_peer('alice') mock_print.assert_any_call('❌ Usage: add_peer ') @patch('builtins.print') def test_do_add_peer_success(self, mock_print): self.cli.api_client.request.return_value = {'message': 'Added'} self.cli.do_add_peer('alice 10.0.0.2 abc123key') mock_print.assert_any_call('✅ Added') @patch('builtins.print') def test_do_add_peer_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_add_peer('alice 10.0.0.2 abc123key') mock_print.assert_any_call('❌ Failed to add peer') # ── do_remove_peer ──────────────────────────────────────────────────────── @patch('builtins.print') def test_do_remove_peer_no_arg(self, mock_print): self.cli.do_remove_peer('') mock_print.assert_any_call('❌ Usage: remove_peer ') @patch('builtins.print') def test_do_remove_peer_success(self, mock_print): self.cli.api_client.request.return_value = {'message': 'Removed'} self.cli.do_remove_peer('alice') mock_print.assert_any_call('✅ Removed') @patch('builtins.print') def test_do_remove_peer_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_remove_peer('alice') mock_print.assert_any_call('❌ Failed to remove peer') # ── do_config ───────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_config_shows_config(self, mock_print): self.cli.api_client.request.return_value = {'cell_name': 'mycel'} self.cli.do_config('') self.assertTrue(any('cell_name' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_config_error_on_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_config('') mock_print.assert_any_call('❌ Failed to get configuration') # ── do_update_config ────────────────────────────────────────────────────── @patch('builtins.print') def test_do_update_config_too_few_args(self, mock_print): self.cli.do_update_config('cell_name') mock_print.assert_any_call('❌ Usage: update_config ') @patch('builtins.print') def test_do_update_config_success(self, mock_print): self.cli.api_client.request.return_value = {'message': 'Updated'} self.cli.do_update_config('cell_name newcell') mock_print.assert_any_call('✅ Updated') @patch('builtins.print') def test_do_update_config_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_update_config('cell_name newcell') mock_print.assert_any_call('❌ Failed to update configuration') # ── do_logs ─────────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_logs_with_log_data(self, mock_print): self.cli.api_client.request.return_value = {'log': 'line1\nline2\n'} self.cli.do_logs('api 10') self.assertTrue(any('line1' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_logs_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_logs('') mock_print.assert_any_call('❌ Failed to get logs') # ── do_health ───────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_health_shows_history(self, mock_print): self.cli.api_client.request.return_value = [ {'timestamp': '2026-01-01T00:00:00', 'alerts': ['disk full']} ] self.cli.do_health('') self.assertTrue(any('disk full' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_health_error(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_health('') mock_print.assert_any_call('❌ Failed to get health data') # ── do_backup / do_restore / do_backups ─────────────────────────────────── @patch('builtins.print') def test_do_backup_success(self, mock_print): self.cli.api_client.request.return_value = {'backup_id': 'bk123'} self.cli.do_backup('') mock_print.assert_any_call('✅ Backup created: bk123') @patch('builtins.print') def test_do_backup_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_backup('') mock_print.assert_any_call('❌ Failed to create backup') @patch('builtins.print') def test_do_restore_no_arg(self, mock_print): self.cli.do_restore('') mock_print.assert_any_call('❌ Usage: restore ') @patch('builtins.print') def test_do_restore_success(self, mock_print): self.cli.api_client.request.return_value = {'ok': True} self.cli.do_restore('bk123') mock_print.assert_any_call('✅ Configuration restored from backup: bk123') @patch('builtins.print') def test_do_restore_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_restore('bk123') mock_print.assert_any_call('❌ Failed to restore configuration') @patch('builtins.print') def test_do_backups_success(self, mock_print): self.cli.api_client.request.return_value = [ {'backup_id': 'bk1', 'timestamp': '2026-01-01', 'services': ['dns']} ] self.cli.do_backups('') self.assertTrue(any('bk1' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_do_backups_failure(self, mock_print): self.cli.api_client.request.return_value = None self.cli.do_backups('') mock_print.assert_any_call('❌ Failed to get backups') # ── do_service ──────────────────────────────────────────────────────────── @patch('builtins.print') def test_do_service_no_arg(self, mock_print): self.cli.do_service('') mock_print.assert_any_call('❌ Usage: service ') @patch('builtins.print') def test_do_service_sets_context(self, mock_print): self.cli.do_service('email') self.assertEqual(self.cli.current_service, 'email') self.assertEqual(self.cli.prompt, 'picell:email> ') # ── do_exit / do_quit / do_EOF ─────────────────────────────────────────── @patch('builtins.print') def test_do_exit_returns_true(self, mock_print): result = self.cli.do_exit('') self.assertTrue(result) @patch('builtins.print') def test_do_quit_delegates_to_exit(self, mock_print): result = self.cli.do_quit('') self.assertTrue(result) @patch('builtins.print') def test_do_eof_returns_true(self, mock_print): result = self.cli.do_EOF('') self.assertTrue(result) # ── show_status / list_services / show_config ───────────────────────────── @patch('builtins.print') def test_show_status(self, mock_print): self.cli.api_client.get = MagicMock(return_value={'cell_name': 'mycel', 'peers_count': 1}) self.cli.show_status() self.assertTrue(any('mycel' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_show_status_handles_none(self, mock_print): self.cli.api_client.get = MagicMock(return_value=None) self.cli.show_status() # Should not raise @patch('builtins.print') def test_list_services(self, mock_print): self.cli.api_client.get = MagicMock(return_value={'email': {'running': True}}) self.cli.list_services() mock_print.assert_called_once() @patch('builtins.print') def test_show_config(self, mock_print): self.cli.api_client.get = MagicMock(return_value={'cell_name': 'mycel'}) self.cli.show_config() self.assertTrue(any('cell_name' in str(c) for c in mock_print.call_args_list)) # ── batch_start_services / batch_stop_services ──────────────────────────── @patch('builtins.print') def test_batch_start_services(self, mock_print): self.cli.api_client.post = MagicMock(return_value={'ok': True}) self.cli.batch_start_services(['email', 'dns']) self.assertEqual(self.cli.api_client.post.call_count, 2) @patch('builtins.print') def test_batch_stop_services(self, mock_print): self.cli.api_client.post = MagicMock(return_value={'ok': True}) self.cli.batch_stop_services(['email']) self.assertEqual(self.cli.api_client.post.call_count, 1) class TestDisplayMethods(unittest.TestCase): def setUp(self): self.cli = EnhancedCLI.__new__(EnhancedCLI) @patch('builtins.print') def test_display_status_with_list_services(self, mock_print): self.cli._display_status({'cell_name': 'mycel', 'services': ['dns', 'dhcp']}) self.assertTrue(any('dns' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_display_status_with_dict_services(self, mock_print): self.cli._display_status({ 'cell_name': 'mycel', 'services': { 'email': {'running': True, 'status': 'online'}, 'dns': False # non-dict service } }) self.assertTrue(any('email' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_display_services_with_non_dict_status(self, mock_print): self.cli._display_services({'email': True, 'timestamp': '2026-01-01'}) self.assertTrue(any('email' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_display_peers(self, mock_print): self.cli._display_peers([ {'name': 'alice', 'ip': '10.0.0.2', 'public_key': 'abcdefghijklmnopqrst', 'added_at': '2026-01-01'} ]) self.assertTrue(any('alice' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_display_health_with_alerts(self, mock_print): self.cli._display_health([ {'timestamp': '2026-01-01T00:00:00', 'alerts': ['disk full']} ]) self.assertTrue(any('disk full' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_display_health_no_alerts(self, mock_print): self.cli._display_health([ {'timestamp': '2026-01-01T00:00:00', 'alerts': []} ]) self.assertTrue(any('2026-01-01' in str(c) for c in mock_print.call_args_list)) class TestModuleLevelFunctions(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tmp) @patch('builtins.print') def test_batch_operations(self, mock_print): """batch_operations executes commands via EnhancedCLI.onecmd.""" # Mock requests to avoid actual HTTP calls with patch('enhanced_cli.requests.get', side_effect=Exception('no server')): batch_operations(['status', 'config']) # Should have printed headers for both commands self.assertTrue(mock_print.call_count >= 2) def test_export_config_json(self): with patch('enhanced_cli.ConfigManager.__init__', lambda self, *a, **kw: setattr(self, 'config', {'key': 'val'}) or None): with patch.object(ConfigManager, '_load_config', return_value={'key': 'val'}): result = export_config('json') self.assertIn('key', result) def test_import_config_success(self): config_file = os.path.join(self.tmp, 'config.json') with open(config_file, 'w') as f: json.dump({'key': 'val'}, f) # Use real ConfigManager with temp dir with patch('enhanced_cli.ConfigManager') as MockCM: mock_instance = MagicMock() MockCM.return_value = mock_instance result = import_config(config_file, 'json') self.assertTrue(result) mock_instance.import_config.assert_called_once() def test_import_config_nonexistent_file_returns_false(self): result = import_config('/nonexistent/config.json', 'json') self.assertFalse(result) class TestConfigManagerJsonPath(unittest.TestCase): """Cover ConfigManager branches that use .json suffix.""" def setUp(self): self.tmp = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tmp) def test_init_with_json_path_sets_config_file_directly(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) self.assertEqual(str(cm.config_file), p) self.assertEqual(cm.config_dir, cm.config_file.parent) def test_load_config_reads_json_file(self): p = os.path.join(self.tmp, 'cfg.json') with open(p, 'w') as f: json.dump({'hello': 'world'}, f) cm = ConfigManager(p) self.assertEqual(cm.config.get('hello'), 'world') def test_load_config_exception_returns_empty(self): p = os.path.join(self.tmp, 'cfg.json') # Write invalid JSON with open(p, 'w') as f: f.write('not json {{') cm = ConfigManager(p) self.assertEqual(cm.config, {}) def test_save_config_writes_json(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) cm.config = {'saved': True} cm.save() with open(p) as f: data = json.load(f) self.assertTrue(data.get('saved')) def test_save_config_exception_does_not_raise(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) # Make the file unwritable by mocking open to raise with patch('builtins.open', side_effect=OSError('disk full')): cm.save() # must not raise def test_export_config_yaml_format(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) cm.config = {'key': 'value'} result = cm.export_config('yaml') self.assertIn('key', result) def test_export_config_unsupported_format_raises(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) with self.assertRaises(ValueError): cm.export_config('xml') def test_import_config_yaml(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) cm.import_config('key: value\n', 'yaml') self.assertEqual(cm.config.get('key'), 'value') def test_import_config_unsupported_format_prints_error(self): p = os.path.join(self.tmp, 'cfg.json') cm = ConfigManager(p) # Should not raise even with unsupported format (caught internally) with patch('builtins.print') as mock_print: cm.import_config('...', 'xml') self.assertTrue(any('Error' in str(c) for c in mock_print.call_args_list)) class TestEnhancedCLIGetPost(unittest.TestCase): """Cover EnhancedCLI.get() and .post() HTTP shortcut methods.""" def setUp(self): self.cli = EnhancedCLI.__new__(EnhancedCLI) self.cli.api_client = MagicMock() self.cli.api_client.base_url = 'http://localhost:3000/api' @patch('enhanced_cli.requests.get') def test_get_returns_json_on_success(self, mock_get): mock_resp = MagicMock() mock_resp.json.return_value = {'ok': True} mock_resp.raise_for_status.return_value = None mock_get.return_value = mock_resp result = self.cli.get('/status') self.assertEqual(result, {'ok': True}) @patch('enhanced_cli.requests.get') def test_get_returns_none_on_exception(self, mock_get): mock_get.side_effect = Exception('connection refused') result = self.cli.get('/status') self.assertIsNone(result) @patch('enhanced_cli.requests.post') def test_post_returns_json_on_success(self, mock_post): mock_resp = MagicMock() mock_resp.json.return_value = {'created': True} mock_resp.raise_for_status.return_value = None mock_post.return_value = mock_resp result = self.cli.post('/peers', {'name': 'alice'}) self.assertEqual(result, {'created': True}) @patch('enhanced_cli.requests.post') def test_post_returns_none_on_exception(self, mock_post): mock_post.side_effect = Exception('timeout') result = self.cli.post('/peers', {'name': 'alice'}) self.assertIsNone(result) class TestShowStatusExceptionPath(unittest.TestCase): """Cover show_status() exception branch.""" def setUp(self): self.cli = EnhancedCLI.__new__(EnhancedCLI) self.cli.api_client = MagicMock() @patch('builtins.print') def test_show_status_exception_prints_error(self, mock_print): self.cli.api_client.get.side_effect = RuntimeError('api down') self.cli.show_status() self.assertTrue(any('Error' in str(c) for c in mock_print.call_args_list)) class TestInteractiveMode(unittest.TestCase): """Cover interactive_mode() loop.""" def setUp(self): self.cli = EnhancedCLI.__new__(EnhancedCLI) self.cli.api_client = MagicMock() self.cli.config_manager = MagicMock() self.cli.current_service = None self.cli.prompt = 'picell> ' @patch('builtins.print') def test_interactive_mode_exits_on_quit(self, mock_print): with patch('builtins.input', side_effect=['quit']): self.cli.interactive_mode() mock_print.assert_any_call('Entering interactive mode. Type \'quit\' to exit.') @patch('builtins.print') def test_interactive_mode_exits_on_eof(self, mock_print): with patch('builtins.input', side_effect=EOFError): self.cli.interactive_mode() class TestMainFunction(unittest.TestCase): """Cover main() argument branches.""" def _run_main(self, args): import sys as _sys old_argv = _sys.argv _sys.argv = ['enhanced_cli'] + args try: from enhanced_cli import main with patch('builtins.print'): try: main() except SystemExit: pass finally: _sys.argv = old_argv def test_main_no_args_prints_help(self): with patch('enhanced_cli.argparse.ArgumentParser.print_help') as mock_help: self._run_main([]) mock_help.assert_called_once() def test_main_status_flag(self): with patch('enhanced_cli.EnhancedCLI') as MockCLI: mock_cli = MagicMock() MockCLI.return_value = mock_cli self._run_main(['--status']) mock_cli.do_status.assert_called_once_with('') def test_main_services_flag(self): with patch('enhanced_cli.EnhancedCLI') as MockCLI: mock_cli = MagicMock() MockCLI.return_value = mock_cli self._run_main(['--services']) mock_cli.do_services.assert_called_once_with('') def test_main_peers_flag(self): with patch('enhanced_cli.EnhancedCLI') as MockCLI: mock_cli = MagicMock() MockCLI.return_value = mock_cli self._run_main(['--peers']) mock_cli.do_peers.assert_called_once_with('') def test_main_logs_flag(self): with patch('enhanced_cli.EnhancedCLI') as MockCLI: mock_cli = MagicMock() MockCLI.return_value = mock_cli self._run_main(['--logs', 'api']) mock_cli.do_logs.assert_called_once_with('api') def test_main_health_flag(self): with patch('enhanced_cli.EnhancedCLI') as MockCLI: mock_cli = MagicMock() MockCLI.return_value = mock_cli self._run_main(['--health']) mock_cli.do_health.assert_called_once_with('') def test_main_batch_flag(self): with patch('enhanced_cli.batch_operations') as mock_batch: self._run_main(['--batch', 'status', 'config']) mock_batch.assert_called_once_with(['status', 'config']) def test_main_export_config_flag(self): with patch('enhanced_cli.export_config', return_value='{}') as mock_export: self._run_main(['--export-config', 'json']) mock_export.assert_called_once_with('json') def test_main_import_config_json_file(self): with patch('enhanced_cli.import_config', return_value=True) as mock_import: self._run_main(['--import-config', 'config.json']) mock_import.assert_called_once_with('config.json', 'json') def test_main_import_config_yaml_file(self): with patch('enhanced_cli.import_config', return_value=True) as mock_import: self._run_main(['--import-config', 'config.yaml']) mock_import.assert_called_once_with('config.yaml', 'yaml') def test_main_wizard_flag(self): with patch('enhanced_cli.service_wizard') as mock_wizard: self._run_main(['--wizard', 'email']) mock_wizard.assert_called_once_with('email') class TestServiceWizardFunction(unittest.TestCase): """Cover service_wizard() branches.""" def _call_wizard(self, service, inputs): from enhanced_cli import service_wizard with patch('builtins.input', side_effect=inputs): with patch('builtins.print'): with patch('enhanced_cli.APIClient') as MockAPI: mock_client = MagicMock() mock_client.request.return_value = {'ok': True} MockAPI.return_value = mock_client service_wizard(service) return mock_client def test_service_wizard_network_calls_api(self): client = self._call_wizard('network', ['53', '10.0.0.100-200', '', '']) client.request.assert_called_once() def test_service_wizard_wireguard_calls_api(self): client = self._call_wizard('wireguard', ['51820', '10.0.0.1/24']) client.request.assert_called_once() def test_service_wizard_email_calls_api(self): client = self._call_wizard('email', ['example.com', '587', '993']) client.request.assert_called_once() @patch('builtins.print') def test_service_wizard_unknown_service_prints_error(self, mock_print): from enhanced_cli import service_wizard service_wizard('unknown_service') self.assertTrue(any('Wizard not available' in str(c) for c in mock_print.call_args_list)) @patch('builtins.print') def test_service_wizard_api_failure_prints_error(self, mock_print): from enhanced_cli import service_wizard with patch('builtins.input', side_effect=['53', '10.0.0.100-200', '', '']): with patch('enhanced_cli.APIClient') as MockAPI: mock_client = MagicMock() mock_client.request.return_value = None MockAPI.return_value = mock_client service_wizard('network') self.assertTrue(any('Failed' in str(c) for c in mock_print.call_args_list)) if __name__ == '__main__': unittest.main()