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>
224 lines
8.2 KiB
Python
224 lines
8.2 KiB
Python
#!/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()
|