aa1e5c41ec
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>
391 lines
16 KiB
Python
391 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Additional tests for cell_cli.py covering the functions NOT in test_cli_tool.py:
|
|
- list_peers (error path)
|
|
- list_nat_rules / add_nat_rule / delete_nat_rule
|
|
- list_peer_routes / add_peer_route / delete_peer_route
|
|
- list_firewall_rules / add_firewall_rule / delete_firewall_rule
|
|
- show_services_status
|
|
- list_wireguard_peers
|
|
- show_network_info / show_dns_status / show_ntp_status
|
|
- main() command routing
|
|
"""
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
api_dir = Path(__file__).parent.parent / 'api'
|
|
sys.path.insert(0, str(api_dir))
|
|
|
|
from cell_cli import (
|
|
list_peers, add_peer, remove_peer, show_config, update_config,
|
|
list_nat_rules, add_nat_rule, delete_nat_rule,
|
|
list_peer_routes, add_peer_route, delete_peer_route,
|
|
list_firewall_rules, add_firewall_rule, delete_firewall_rule,
|
|
show_services_status, list_wireguard_peers,
|
|
show_network_info, show_dns_status, show_ntp_status,
|
|
)
|
|
|
|
|
|
class TestListPeersErrorPath(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_list_peers_failure_prints_error(self, mock_req, mock_print):
|
|
list_peers()
|
|
mock_print.assert_any_call('Failed to fetch peers.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=[])
|
|
def test_list_peers_empty_list(self, mock_req, mock_print):
|
|
list_peers()
|
|
mock_print.assert_any_call('No peers configured.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=[
|
|
{'name': 'alice', 'ip': '10.0.0.2',
|
|
'public_key': 'abcdefghijklmnopqrstuvwxyz', 'added_at': '2026-01-01'}
|
|
])
|
|
def test_list_peers_shows_peer_info(self, mock_req, mock_print):
|
|
list_peers()
|
|
self.assertTrue(any('alice' in str(c) for c in mock_print.call_args_list))
|
|
|
|
|
|
class TestNatRules(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'nat_rules': []})
|
|
def test_list_nat_rules_empty(self, mock_req, mock_print):
|
|
list_nat_rules()
|
|
mock_print.assert_any_call('No NAT rules configured.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'nat_rules': [
|
|
{'id': 1, 'source_network': '10.0.0.0/24', 'target_interface': 'eth0',
|
|
'masquerade': True, 'nat_type': 'MASQUERADE', 'protocol': 'ALL',
|
|
'external_port': '', 'internal_ip': '', 'internal_port': ''}
|
|
]})
|
|
def test_list_nat_rules_shows_rules(self, mock_req, mock_print):
|
|
list_nat_rules()
|
|
self.assertTrue(any('eth0' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_list_nat_rules_failure(self, mock_req, mock_print):
|
|
list_nat_rules()
|
|
mock_print.assert_any_call('Failed to fetch NAT rules.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'id': 1})
|
|
def test_add_nat_rule_success(self, mock_req, mock_print):
|
|
add_nat_rule('10.0.0.0/24', 'eth0', True, 'MASQUERADE', 'ALL', '', '', '')
|
|
mock_print.assert_any_call('✅ NAT rule added.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_add_nat_rule_failure(self, mock_req, mock_print):
|
|
add_nat_rule('10.0.0.0/24', 'eth0', False, 'DNAT', 'TCP', '80', '10.0.0.5', '8080')
|
|
mock_print.assert_any_call('❌ Failed to add NAT rule.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'ok': True})
|
|
def test_delete_nat_rule_success(self, mock_req, mock_print):
|
|
delete_nat_rule(1)
|
|
mock_print.assert_any_call('✅ NAT rule deleted.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_delete_nat_rule_failure(self, mock_req, mock_print):
|
|
delete_nat_rule(99)
|
|
mock_print.assert_any_call('❌ Failed to delete NAT rule.')
|
|
|
|
|
|
class TestPeerRoutes(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'peer_routes': []})
|
|
def test_list_peer_routes_empty(self, mock_req, mock_print):
|
|
list_peer_routes()
|
|
mock_print.assert_any_call('No peer routes configured.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'peer_routes': [
|
|
{'peer_name': 'alice', 'peer_ip': '10.0.0.2',
|
|
'allowed_networks': ['192.168.1.0/24'], 'route_type': 'split'}
|
|
]})
|
|
def test_list_peer_routes_shows_routes(self, mock_req, mock_print):
|
|
list_peer_routes()
|
|
self.assertTrue(any('alice' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_list_peer_routes_failure(self, mock_req, mock_print):
|
|
list_peer_routes()
|
|
mock_print.assert_any_call('Failed to fetch peer routes.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'ok': True})
|
|
def test_add_peer_route_success(self, mock_req, mock_print):
|
|
add_peer_route('alice', '10.0.0.2', '192.168.1.0/24', 'split')
|
|
mock_print.assert_any_call('✅ Peer route added.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_add_peer_route_failure(self, mock_req, mock_print):
|
|
add_peer_route('alice', '10.0.0.2', '192.168.1.0/24', 'split')
|
|
mock_print.assert_any_call('❌ Failed to add peer route.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'ok': True})
|
|
def test_delete_peer_route_success(self, mock_req, mock_print):
|
|
delete_peer_route('alice')
|
|
mock_print.assert_any_call('✅ Peer route deleted.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_delete_peer_route_failure(self, mock_req, mock_print):
|
|
delete_peer_route('alice')
|
|
mock_print.assert_any_call('❌ Failed to delete peer route.')
|
|
|
|
|
|
class TestFirewallRules(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'firewall_rules': []})
|
|
def test_list_firewall_rules_empty(self, mock_req, mock_print):
|
|
list_firewall_rules()
|
|
mock_print.assert_any_call('No firewall rules configured.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'firewall_rules': [
|
|
{'id': 1, 'rule_type': 'ACCEPT', 'source': '10.0.0.0/24',
|
|
'destination': 'any', 'protocol': 'TCP', 'port_range': '80', 'action': 'ACCEPT'}
|
|
]})
|
|
def test_list_firewall_rules_shows_rules(self, mock_req, mock_print):
|
|
list_firewall_rules()
|
|
self.assertTrue(any('ACCEPT' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_list_firewall_rules_failure(self, mock_req, mock_print):
|
|
list_firewall_rules()
|
|
mock_print.assert_any_call('Failed to fetch firewall rules.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'id': 1})
|
|
def test_add_firewall_rule_success(self, mock_req, mock_print):
|
|
add_firewall_rule('ACCEPT', '10.0.0.0/24', 'any', 'ACCEPT', 'TCP', '80')
|
|
mock_print.assert_any_call('✅ Firewall rule added.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_add_firewall_rule_failure(self, mock_req, mock_print):
|
|
add_firewall_rule('DROP', 'any', 'any', 'DROP', 'ALL', '')
|
|
mock_print.assert_any_call('❌ Failed to add firewall rule.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'ok': True})
|
|
def test_delete_firewall_rule_success(self, mock_req, mock_print):
|
|
delete_firewall_rule(1)
|
|
mock_print.assert_any_call('✅ Firewall rule deleted.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_delete_firewall_rule_failure(self, mock_req, mock_print):
|
|
delete_firewall_rule(99)
|
|
mock_print.assert_any_call('❌ Failed to delete firewall rule.')
|
|
|
|
|
|
class TestShowServicesStatus(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={
|
|
'email': {'status': 'online', 'running': True},
|
|
'dns': True
|
|
})
|
|
def test_show_services_status_with_dict_and_bool(self, mock_req, mock_print):
|
|
show_services_status()
|
|
self.assertTrue(any('email' in str(c) for c in mock_print.call_args_list))
|
|
self.assertTrue(any('dns' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_show_services_status_failure(self, mock_req, mock_print):
|
|
show_services_status()
|
|
mock_print.assert_any_call('Failed to fetch service status.')
|
|
|
|
|
|
class TestListWireguardPeers(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=[
|
|
{'name': 'alice', 'public_key': 'pk1', 'ip': '10.0.0.2', 'status': 'active'}
|
|
])
|
|
def test_list_wireguard_peers_shows_peers(self, mock_req, mock_print):
|
|
list_wireguard_peers()
|
|
self.assertTrue(any('alice' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_list_wireguard_peers_failure(self, mock_req, mock_print):
|
|
list_wireguard_peers()
|
|
mock_print.assert_any_call('Failed to fetch WireGuard peers.')
|
|
|
|
|
|
class TestNetworkDnsNtpStatus(unittest.TestCase):
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'gateway': '192.168.1.1', 'subnet': '10.0.0.0/24'})
|
|
def test_show_network_info_success(self, mock_req, mock_print):
|
|
show_network_info()
|
|
self.assertTrue(any('gateway' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_show_network_info_failure(self, mock_req, mock_print):
|
|
show_network_info()
|
|
mock_print.assert_any_call('Failed to fetch network info.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'running': True, 'port': 53})
|
|
def test_show_dns_status_success(self, mock_req, mock_print):
|
|
show_dns_status()
|
|
self.assertTrue(any('running' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_show_dns_status_failure(self, mock_req, mock_print):
|
|
show_dns_status()
|
|
mock_print.assert_any_call('Failed to fetch DNS status.')
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value={'synced': True, 'server': 'pool.ntp.org'})
|
|
def test_show_ntp_status_success(self, mock_req, mock_print):
|
|
show_ntp_status()
|
|
self.assertTrue(any('synced' in str(c) for c in mock_print.call_args_list))
|
|
|
|
@patch('builtins.print')
|
|
@patch('cell_cli.api_request', return_value=None)
|
|
def test_show_ntp_status_failure(self, mock_req, mock_print):
|
|
show_ntp_status()
|
|
mock_print.assert_any_call('Failed to fetch NTP status.')
|
|
|
|
|
|
class TestMainFunction(unittest.TestCase):
|
|
"""Cover main() by patching individual functions and simulating command dispatch."""
|
|
|
|
def _run_main(self, args):
|
|
import sys as _sys
|
|
from cell_cli import main
|
|
old_argv = _sys.argv
|
|
_sys.argv = ['cell_cli'] + args
|
|
try:
|
|
with patch('builtins.print'):
|
|
try:
|
|
main()
|
|
except SystemExit:
|
|
pass
|
|
finally:
|
|
_sys.argv = old_argv
|
|
|
|
def test_main_status_command(self):
|
|
with patch('cell_cli.show_status') as mock_fn:
|
|
self._run_main(['status'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_peers_list_command(self):
|
|
with patch('cell_cli.list_peers') as mock_fn:
|
|
self._run_main(['peers', 'list'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_peers_add_command(self):
|
|
with patch('cell_cli.add_peer') as mock_fn:
|
|
self._run_main(['peers', 'add', 'alice', '10.0.0.2', 'pubkey'])
|
|
mock_fn.assert_called_once_with('alice', '10.0.0.2', 'pubkey')
|
|
|
|
def test_main_peers_remove_command(self):
|
|
with patch('cell_cli.remove_peer') as mock_fn:
|
|
self._run_main(['peers', 'remove', 'alice'])
|
|
mock_fn.assert_called_once_with('alice')
|
|
|
|
def test_main_config_show_command(self):
|
|
with patch('cell_cli.show_config') as mock_fn:
|
|
self._run_main(['config', 'show'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_config_update_command(self):
|
|
with patch('cell_cli.update_config') as mock_fn:
|
|
self._run_main(['config', 'update', 'cell_name', 'mycell'])
|
|
mock_fn.assert_called_once_with('cell_name', 'mycell')
|
|
|
|
def test_main_routing_nat_list(self):
|
|
with patch('cell_cli.list_nat_rules') as mock_fn:
|
|
self._run_main(['routing', 'nat', 'list'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_nat_add(self):
|
|
with patch('cell_cli.add_nat_rule') as mock_fn:
|
|
self._run_main(['routing', 'nat', 'add', '10.0.0.0/24', 'eth0'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_nat_delete(self):
|
|
with patch('cell_cli.delete_nat_rule') as mock_fn:
|
|
self._run_main(['routing', 'nat', 'delete', '1'])
|
|
mock_fn.assert_called_once_with('1') # argparse passes as string
|
|
|
|
def test_main_routing_peers_list(self):
|
|
with patch('cell_cli.list_peer_routes') as mock_fn:
|
|
self._run_main(['routing', 'peers', 'list'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_peers_add(self):
|
|
with patch('cell_cli.add_peer_route') as mock_fn:
|
|
self._run_main(['routing', 'peers', 'add', 'alice', '10.0.0.2',
|
|
'192.168.1.0/24'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_peers_delete(self):
|
|
with patch('cell_cli.delete_peer_route') as mock_fn:
|
|
self._run_main(['routing', 'peers', 'delete', 'alice'])
|
|
mock_fn.assert_called_once_with('alice')
|
|
|
|
def test_main_routing_firewall_list(self):
|
|
with patch('cell_cli.list_firewall_rules') as mock_fn:
|
|
self._run_main(['routing', 'firewall', 'list'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_firewall_add(self):
|
|
with patch('cell_cli.add_firewall_rule') as mock_fn:
|
|
self._run_main(['routing', 'firewall', 'add',
|
|
'ACCEPT', '10.0.0.0/24', 'any', 'ACCEPT'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_routing_firewall_delete(self):
|
|
with patch('cell_cli.delete_firewall_rule') as mock_fn:
|
|
self._run_main(['routing', 'firewall', 'delete', '1'])
|
|
mock_fn.assert_called_once_with('1')
|
|
|
|
def test_main_services_status_command(self):
|
|
with patch('cell_cli.show_services_status') as mock_fn:
|
|
self._run_main(['services-status'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_wireguard_list_command(self):
|
|
with patch('cell_cli.list_wireguard_peers') as mock_fn:
|
|
self._run_main(['wireguard-peers'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_network_info_command(self):
|
|
with patch('cell_cli.show_network_info') as mock_fn:
|
|
self._run_main(['network-info'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_dns_status_command(self):
|
|
with patch('cell_cli.show_dns_status') as mock_fn:
|
|
self._run_main(['dns-status'])
|
|
mock_fn.assert_called_once()
|
|
|
|
def test_main_ntp_status_command(self):
|
|
with patch('cell_cli.show_ntp_status') as mock_fn:
|
|
self._run_main(['ntp-status'])
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|