#!/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()