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,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Additional tests for ServiceBus covering missed branches:
|
||||
- unsubscribe_from_event: handler-not-found ValueError
|
||||
- call_service: method raises exception (publishes ERROR_OCCURRED, re-raises)
|
||||
- orchestrate_service_start/stop: with container manager registered
|
||||
- orchestrate_service_restart: exception path
|
||||
- _event_loop: processing events through handlers, handler exception
|
||||
- add_service_dependency: new service key branch
|
||||
- remove_service_dependency: dependency not found (ValueError branch)
|
||||
- get_service_status_summary: service without get_status, service that raises
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, MagicMock
|
||||
|
||||
api_dir = Path(__file__).parent.parent / 'api'
|
||||
sys.path.insert(0, str(api_dir))
|
||||
|
||||
from service_bus import ServiceBus, EventType, Event
|
||||
|
||||
|
||||
class TestUnsubscribeNotFound(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bus = ServiceBus()
|
||||
|
||||
def test_unsubscribe_handler_not_registered_does_not_raise(self):
|
||||
# unsubscribing a handler that was never added should not raise
|
||||
def handler(event):
|
||||
pass
|
||||
# should not raise ValueError
|
||||
self.bus.unsubscribe_from_event(EventType.SERVICE_STARTED, handler)
|
||||
|
||||
def test_unsubscribe_removes_correct_handler(self):
|
||||
received = []
|
||||
def h1(event): received.append('h1')
|
||||
def h2(event): received.append('h2')
|
||||
self.bus.subscribe_to_event(EventType.SERVICE_STARTED, h1)
|
||||
self.bus.subscribe_to_event(EventType.SERVICE_STARTED, h2)
|
||||
self.bus.unsubscribe_from_event(EventType.SERVICE_STARTED, h1)
|
||||
handlers = self.bus.event_handlers[EventType.SERVICE_STARTED]
|
||||
self.assertNotIn(h1, handlers)
|
||||
self.assertIn(h2, handlers)
|
||||
|
||||
|
||||
class TestCallServiceException(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bus = ServiceBus()
|
||||
|
||||
def test_call_service_method_raises_publishes_error_and_reraises(self):
|
||||
mock_svc = Mock()
|
||||
mock_svc.failing_method.side_effect = RuntimeError("boom")
|
||||
self.bus.register_service('svc', mock_svc)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.bus.call_service('svc', 'failing_method')
|
||||
|
||||
def test_call_service_error_is_added_to_queue(self):
|
||||
mock_svc = Mock()
|
||||
mock_svc.boom.side_effect = ValueError("bad value")
|
||||
self.bus.register_service('svc', mock_svc)
|
||||
|
||||
try:
|
||||
self.bus.call_service('svc', 'boom')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# The ERROR_OCCURRED event was put onto the queue
|
||||
self.assertFalse(self.bus.event_queue.empty())
|
||||
|
||||
|
||||
class TestOrchestrateWithContainers(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bus = ServiceBus()
|
||||
|
||||
def _register_container_manager(self, start_return=True, stop_return=True):
|
||||
cm = Mock()
|
||||
cm.start_container.return_value = start_return
|
||||
cm.stop_container.return_value = stop_return
|
||||
self.bus.register_service('container', cm)
|
||||
return cm
|
||||
|
||||
def test_orchestrate_start_wireguard_starts_containers(self):
|
||||
cm = self._register_container_manager(start_return=True)
|
||||
# wireguard service has containers but is not registered as a service
|
||||
result = self.bus.orchestrate_service_start('wireguard')
|
||||
self.assertTrue(result)
|
||||
cm.start_container.assert_called_with('cell-wireguard')
|
||||
|
||||
def test_orchestrate_start_container_failure_returns_false(self):
|
||||
cm = self._register_container_manager(start_return=False)
|
||||
result = self.bus.orchestrate_service_start('wireguard')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_orchestrate_start_no_container_manager_returns_false(self):
|
||||
# email has containers but 'container' manager is not registered
|
||||
result = self.bus.orchestrate_service_start('email')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_orchestrate_stop_wireguard_stops_containers(self):
|
||||
cm = self._register_container_manager(stop_return=True)
|
||||
result = self.bus.orchestrate_service_stop('wireguard')
|
||||
self.assertTrue(result)
|
||||
cm.stop_container.assert_called_with('cell-wireguard')
|
||||
|
||||
def test_orchestrate_stop_container_failure_returns_false(self):
|
||||
cm = self._register_container_manager(stop_return=False)
|
||||
result = self.bus.orchestrate_service_stop('wireguard')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_orchestrate_stop_no_container_manager_returns_false(self):
|
||||
result = self.bus.orchestrate_service_stop('email')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_orchestrate_restart_exception_returns_false(self):
|
||||
# Make stop raise an exception to trigger the except clause
|
||||
cm = Mock()
|
||||
cm.stop_container.side_effect = RuntimeError("docker gone")
|
||||
self.bus.register_service('container', cm)
|
||||
result = self.bus.orchestrate_service_restart('wireguard')
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class TestEventLoopProcessing(unittest.TestCase):
|
||||
def test_event_loop_calls_handler(self):
|
||||
bus = ServiceBus()
|
||||
received = []
|
||||
|
||||
def handler(event):
|
||||
received.append(event)
|
||||
|
||||
bus.subscribe_to_event(EventType.SERVICE_STARTED, handler)
|
||||
bus.start()
|
||||
try:
|
||||
bus.publish_event(EventType.SERVICE_STARTED, 'src', {'x': 1})
|
||||
time.sleep(0.3)
|
||||
self.assertEqual(len(received), 1)
|
||||
self.assertEqual(received[0].source, 'src')
|
||||
finally:
|
||||
bus.stop()
|
||||
|
||||
def test_event_loop_handler_exception_does_not_stop_loop(self):
|
||||
bus = ServiceBus()
|
||||
received = []
|
||||
|
||||
def bad_handler(event):
|
||||
raise RuntimeError("handler crash")
|
||||
|
||||
def good_handler(event):
|
||||
received.append(event)
|
||||
|
||||
bus.subscribe_to_event(EventType.SERVICE_STARTED, bad_handler)
|
||||
bus.subscribe_to_event(EventType.SERVICE_STARTED, good_handler)
|
||||
bus.start()
|
||||
try:
|
||||
bus.publish_event(EventType.SERVICE_STARTED, 'src', {})
|
||||
time.sleep(0.3)
|
||||
# Loop continues; good_handler was also called
|
||||
self.assertEqual(len(received), 1)
|
||||
finally:
|
||||
bus.stop()
|
||||
|
||||
def test_event_loop_history_trimmed_at_max(self):
|
||||
bus = ServiceBus()
|
||||
bus.max_history = 3
|
||||
bus.start()
|
||||
try:
|
||||
for i in range(5):
|
||||
bus.publish_event(EventType.SERVICE_STARTED, f'src{i}', {})
|
||||
time.sleep(0.3)
|
||||
self.assertLessEqual(len(bus.event_history), 3)
|
||||
finally:
|
||||
bus.stop()
|
||||
|
||||
|
||||
class TestServiceDependencyEdgeCases(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bus = ServiceBus()
|
||||
|
||||
def test_add_dependency_creates_new_entry_for_unknown_service(self):
|
||||
self.bus.add_service_dependency('brand_new_service', 'network')
|
||||
self.assertIn('brand_new_service', self.bus.service_dependencies)
|
||||
self.assertIn('network', self.bus.service_dependencies['brand_new_service'])
|
||||
|
||||
def test_remove_dependency_not_found_does_not_raise(self):
|
||||
self.bus.add_service_dependency('svc', 'dep1')
|
||||
# removing a dependency that was never added should not raise
|
||||
self.bus.remove_service_dependency('svc', 'dep_nonexistent')
|
||||
|
||||
def test_remove_dependency_for_unknown_service_does_not_raise(self):
|
||||
# service never had any dependencies registered
|
||||
self.bus.remove_service_dependency('ghost_service', 'dep')
|
||||
|
||||
|
||||
class TestServiceStatusSummaryBranches(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bus = ServiceBus()
|
||||
|
||||
def test_summary_service_without_get_status(self):
|
||||
# A service object without a get_status attribute
|
||||
svc = object()
|
||||
self.bus.register_service('plain_obj', svc)
|
||||
summary = self.bus.get_service_status_summary()
|
||||
self.assertIn('plain_obj', summary['services'])
|
||||
self.assertEqual(
|
||||
summary['services']['plain_obj']['status'],
|
||||
{'status': 'unknown'}
|
||||
)
|
||||
|
||||
def test_summary_service_get_status_raises(self):
|
||||
svc = Mock()
|
||||
svc.get_status.side_effect = RuntimeError("status unavailable")
|
||||
self.bus.register_service('broken_svc', svc)
|
||||
summary = self.bus.get_service_status_summary()
|
||||
self.assertIn('broken_svc', summary['services'])
|
||||
self.assertIn('error', summary['services']['broken_svc']['status'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user