test: raise coverage 68.7% -> ~80.4%; add ~250 tests for new egress/DDNS/network paths
Unit Tests / test (push) Successful in 12m6s

Coverage was below acceptable levels and several newly-added code paths
(sshuttle egress, proxy egress, DDNS provider stubs, DNS overview route,
peer-registry provisioning) had zero test coverage.

~250 new unit tests are added across 16 new test files. Existing test files
are updated to match refactored interfaces (DHCP removed, constants
introduced, network_manager restructured). .coveragerc is added to pin the
source mapping and the 70% floor so regressions are caught at commit time.

tests/test_enhanced_api.py was previously living in api/ (wrong location)
and is moved to tests/ where it belongs.

Integration test files are updated to remove references to DHCP endpoints
and add coverage for the new DNS overview and DDNS sync endpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 09:03:39 -04:00
parent c41cadafb4
commit aa1e5c41ec
33 changed files with 9446 additions and 631 deletions
+696
View File
@@ -0,0 +1,696 @@
#!/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 <name> <ip> <public_key>')
@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 <name>')
@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 <key> <value>')
@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 <backup_id>')
@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 <service_name>')
@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()