test: raise coverage 68.7% -> ~80.4%; add ~250 tests for new egress/DDNS/network paths
Unit Tests / test (push) Successful in 12m6s
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:
@@ -0,0 +1,390 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user