""" Tests for routes/services.py: - /api/services/catalog (list, get, status, restart, reconfigure) - /api/services/catalog//accounts (list, provision, deprovision, credentials) - /api/services/bus/* (status, events, start, stop, restart) - /api/logs/* endpoints """ import sys import json from pathlib import Path from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, str(Path(__file__).parent.parent / 'api')) import app as app_module from app import app @pytest.fixture def client(): app.config['TESTING'] = True with app.test_client() as c: yield c def _make_registry(services): reg = MagicMock() reg.list_all = MagicMock(return_value=services) reg.list_active = MagicMock(return_value=services) reg.get = MagicMock(side_effect=lambda sid: next( (s for s in services if s['id'] == sid), None)) return reg def _make_service(sid, kind='builtin'): return {'id': sid, 'name': sid.title(), 'kind': kind, 'subdomain': sid, 'capabilities': {}} # --------------------------------------------------------------------------- # GET /api/services/catalog # --------------------------------------------------------------------------- class TestGetServicesCatalog: def test_returns_200(self, client): reg = _make_registry([]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog') assert resp.status_code == 200 def test_returns_services_list(self, client): reg = _make_registry([_make_service('email')]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog') data = json.loads(resp.data) assert 'services' in data assert len(data['services']) == 1 def test_returns_empty_when_no_services(self, client): reg = _make_registry([]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog') data = json.loads(resp.data) assert data['services'] == [] def test_500_on_exception(self, client): reg = MagicMock() reg.list_all.side_effect = Exception('registry error') with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/catalog/ # --------------------------------------------------------------------------- class TestGetServiceCatalogEntry: def test_returns_200_for_known_service(self, client): reg = _make_registry([_make_service('email')]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog/email') assert resp.status_code == 200 def test_returns_service_data(self, client): reg = _make_registry([_make_service('email')]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog/email') data = json.loads(resp.data) assert data['id'] == 'email' def test_returns_404_for_unknown_service(self, client): reg = _make_registry([]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog/nonexistent') assert resp.status_code == 404 def test_404_includes_error_message(self, client): reg = _make_registry([]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog/nonexistent') data = json.loads(resp.data) assert 'error' in data def test_500_on_exception(self, client): reg = MagicMock() reg.get.side_effect = Exception('registry error') with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/catalog/email') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/catalog//status # --------------------------------------------------------------------------- class TestGetServiceContainerStatus: def test_returns_200(self, client): svc = _make_service('email') reg = _make_registry([svc]) composer = MagicMock() composer.status_service.return_value = {'running': True, 'containers': []} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.get('/api/services/catalog/email/status') assert resp.status_code == 200 def test_returns_404_for_unknown_service(self, client): reg = _make_registry([]) composer = MagicMock() with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.get('/api/services/catalog/nonexistent/status') assert resp.status_code == 404 def test_400_on_value_error(self, client): svc = _make_service('email') reg = _make_registry([svc]) composer = MagicMock() composer.status_service.side_effect = ValueError('bad service') with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.get('/api/services/catalog/email/status') assert resp.status_code == 400 # --------------------------------------------------------------------------- # POST /api/services/catalog//restart # --------------------------------------------------------------------------- class TestRestartServiceContainers: def test_returns_200_on_success(self, client): svc = _make_service('email') reg = _make_registry([svc]) composer = MagicMock() composer.restart_service.return_value = {'ok': True, 'stdout': ''} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/email/restart') assert resp.status_code == 200 def test_returns_404_for_unknown_service(self, client): reg = _make_registry([]) composer = MagicMock() with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/nonexistent/restart') assert resp.status_code == 404 def test_returns_500_when_restart_fails(self, client): svc = _make_service('email') reg = _make_registry([svc]) composer = MagicMock() composer.restart_service.return_value = {'ok': False, 'stderr': 'container error'} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/email/restart') assert resp.status_code == 500 def test_response_includes_message_on_success(self, client): svc = _make_service('email') reg = _make_registry([svc]) composer = MagicMock() composer.restart_service.return_value = {'ok': True} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/email/restart') data = json.loads(resp.data) assert 'message' in data # --------------------------------------------------------------------------- # POST /api/services/catalog//reconfigure # --------------------------------------------------------------------------- class TestReconfigureService: def test_404_for_unknown_service(self, client): reg = _make_registry([]) composer = MagicMock() with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/nonexistent/reconfigure') assert resp.status_code == 404 def test_400_for_builtin_service(self, client): svc = _make_service('wireguard', kind='builtin') reg = _make_registry([svc]) composer = MagicMock() with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/wireguard/reconfigure') assert resp.status_code == 400 data = json.loads(resp.data) assert 'Builtins' in data['error'] def test_400_when_no_compose_file(self, client): svc = _make_service('myapp', kind='store') reg = _make_registry([svc]) composer = MagicMock() composer.has_compose_file.return_value = False with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/myapp/reconfigure') assert resp.status_code == 400 def test_200_on_success(self, client): svc = _make_service('myapp', kind='store') reg = _make_registry([svc]) composer = MagicMock() composer.has_compose_file.return_value = True composer.up.return_value = {'ok': True, 'stdout': ''} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/myapp/reconfigure') assert resp.status_code == 200 def test_500_when_up_fails(self, client): svc = _make_service('myapp', kind='store') reg = _make_registry([svc]) composer = MagicMock() composer.has_compose_file.return_value = True composer.up.return_value = {'ok': False, 'stderr': 'error'} with patch.object(app_module, 'service_registry', reg), \ patch.object(app_module, 'service_composer', composer): resp = client.post('/api/services/catalog/myapp/reconfigure') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/catalog//accounts # --------------------------------------------------------------------------- class TestListServiceAccounts: def test_returns_200(self, client): am = MagicMock() am.list_accounts.return_value = ['alice', 'bob'] with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts') assert resp.status_code == 200 def test_returns_accounts_list(self, client): am = MagicMock() am.list_accounts.return_value = ['alice'] with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts') data = json.loads(resp.data) assert data['accounts'] == ['alice'] assert data['service_id'] == 'email' def test_500_on_exception(self, client): am = MagicMock() am.list_accounts.side_effect = Exception('fail') with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/services/catalog//accounts # --------------------------------------------------------------------------- class TestProvisionServiceAccount: def test_returns_201_on_success(self, client): am = MagicMock() am.provision.return_value = None with patch.object(app_module, 'account_manager', am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({'username': 'alice'}), content_type='application/json') assert resp.status_code == 201 def test_400_when_username_missing(self, client): am = MagicMock() with patch.object(app_module, 'account_manager', am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({}), content_type='application/json') assert resp.status_code == 400 def test_400_on_value_error(self, client): am = MagicMock() am.provision.side_effect = ValueError('user already exists') with patch.object(app_module, 'account_manager', am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({'username': 'alice'}), content_type='application/json') assert resp.status_code == 400 def test_500_on_runtime_error(self, client): am = MagicMock() am.provision.side_effect = RuntimeError('docker exec failed') with patch.object(app_module, 'account_manager', am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({'username': 'alice'}), content_type='application/json') assert resp.status_code == 500 def test_response_shape(self, client): am = MagicMock() am.provision.return_value = None with patch.object(app_module, 'account_manager', am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({'username': 'alice'}), content_type='application/json') data = json.loads(resp.data) assert data['service_id'] == 'email' assert data['username'] == 'alice' assert data['provisioned'] is True # --------------------------------------------------------------------------- # DELETE /api/services/catalog//accounts/ # --------------------------------------------------------------------------- class TestDeprovisionServiceAccount: def test_returns_200_on_success(self, client): am = MagicMock() am.deprovision.return_value = True with patch.object(app_module, 'account_manager', am): resp = client.delete('/api/services/catalog/email/accounts/alice') assert resp.status_code == 200 def test_returns_500_when_deprovision_fails(self, client): am = MagicMock() am.deprovision.return_value = False with patch.object(app_module, 'account_manager', am): resp = client.delete('/api/services/catalog/email/accounts/alice') assert resp.status_code == 500 def test_400_on_value_error(self, client): am = MagicMock() am.deprovision.side_effect = ValueError('not found') with patch.object(app_module, 'account_manager', am): resp = client.delete('/api/services/catalog/email/accounts/alice') assert resp.status_code == 400 # --------------------------------------------------------------------------- # GET /api/services/catalog//accounts//credentials # --------------------------------------------------------------------------- class TestGetServiceAccountCredentials: def test_returns_200_with_creds(self, client): am = MagicMock() am.get_credentials.return_value = {'password': 'secret'} with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts/alice/credentials') assert resp.status_code == 200 def test_returns_404_when_not_provisioned(self, client): am = MagicMock() am.get_credentials.return_value = None with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts/alice/credentials') assert resp.status_code == 404 def test_returns_creds_in_response(self, client): am = MagicMock() am.get_credentials.return_value = {'password': 'mypass'} with patch.object(app_module, 'account_manager', am): resp = client.get('/api/services/catalog/email/accounts/alice/credentials') data = json.loads(resp.data) assert data['service_id'] == 'email' assert data['username'] == 'alice' assert 'password' in data # --------------------------------------------------------------------------- # GET /api/services/bus/status # --------------------------------------------------------------------------- class TestServiceBusStatus: def test_returns_200(self, client): mock_sb = MagicMock() mock_sb.get_service_status_summary.return_value = {} with patch.object(app_module, 'service_bus', mock_sb): resp = client.get('/api/services/bus/status') assert resp.status_code == 200 def test_500_on_exception(self, client): mock_sb = MagicMock() mock_sb.get_service_status_summary.side_effect = Exception('fail') with patch.object(app_module, 'service_bus', mock_sb): resp = client.get('/api/services/bus/status') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/services/bus/services//start # --------------------------------------------------------------------------- class TestServiceBusStart: def test_returns_200_on_success(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_start.return_value = True with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/network/start') assert resp.status_code == 200 def test_returns_500_on_failure(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_start.return_value = False with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/network/start') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/services/bus/services//stop # --------------------------------------------------------------------------- class TestServiceBusStop: def test_returns_200_on_success(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_stop.return_value = True with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/network/stop') assert resp.status_code == 200 def test_returns_500_on_failure(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_stop.return_value = False with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/network/stop') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/services/bus/services//restart # --------------------------------------------------------------------------- class TestServiceBusRestart: def test_returns_200_on_success(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_restart.return_value = True with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/email/restart') assert resp.status_code == 200 def test_returns_500_on_failure(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_restart.return_value = False with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/email/restart') assert resp.status_code == 500 def test_500_on_exception(self, client): mock_sb = MagicMock() mock_sb.orchestrate_service_restart.side_effect = Exception('bus error') with patch.object(app_module, 'service_bus', mock_sb): resp = client.post('/api/services/bus/services/email/restart') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/bus/events # --------------------------------------------------------------------------- class TestServiceBusEvents: def test_returns_200(self, client): mock_sb = MagicMock() mock_sb.get_event_history.return_value = [] with patch.object(app_module, 'service_bus', mock_sb): resp = client.get('/api/services/bus/events') assert resp.status_code == 200 def test_returns_list(self, client): mock_sb = MagicMock() mock_sb.get_event_history.return_value = [] with patch.object(app_module, 'service_bus', mock_sb): resp = client.get('/api/services/bus/events') data = json.loads(resp.data) assert isinstance(data, list) # --------------------------------------------------------------------------- # GET /api/logs/services/ # --------------------------------------------------------------------------- class TestGetServiceLogs: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.get_service_logs.return_value = ['line1', 'line2'] with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/services/network') assert resp.status_code == 200 def test_returns_log_lines(self, client): mock_lm = MagicMock() mock_lm.get_service_logs.return_value = ['line1'] with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/services/network') data = json.loads(resp.data) assert 'logs' in data assert data['service'] == 'network' def test_500_on_exception(self, client): mock_lm = MagicMock() mock_lm.get_service_logs.side_effect = Exception('fail') with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/services/network') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/logs/search # --------------------------------------------------------------------------- class TestSearchLogs: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.search_logs.return_value = [] with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/search', data=json.dumps({'query': 'error'}), content_type='application/json') assert resp.status_code == 200 def test_returns_results(self, client): mock_lm = MagicMock() mock_lm.search_logs.return_value = [{'message': 'error occurred'}] with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/search', data=json.dumps({'query': 'error'}), content_type='application/json') data = json.loads(resp.data) assert 'results' in data assert 'count' in data assert data['count'] == 1 # --------------------------------------------------------------------------- # GET /api/logs/statistics # --------------------------------------------------------------------------- class TestLogStatistics: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.get_log_statistics.return_value = {'total': 100} with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/statistics') assert resp.status_code == 200 # --------------------------------------------------------------------------- # POST /api/logs/rotate # --------------------------------------------------------------------------- class TestRotateLogs: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.rotate_logs.return_value = None with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/rotate', data=json.dumps({}), content_type='application/json') assert resp.status_code == 200 # --------------------------------------------------------------------------- # GET /api/logs/files # --------------------------------------------------------------------------- class TestGetLogFiles: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.get_all_log_file_infos.return_value = [] with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/files') assert resp.status_code == 200 def test_returns_file_list(self, client): mock_lm = MagicMock() mock_lm.get_all_log_file_infos.return_value = [ {'file': 'network.log', 'size': 1024, 'backup': False} ] with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/files') data = json.loads(resp.data) assert len(data) == 1 assert data[0]['file'] == 'network.log' def test_500_on_exception(self, client): mock_lm = MagicMock() mock_lm.get_all_log_file_infos.side_effect = Exception('disk error') with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/files') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/logs/verbosity # --------------------------------------------------------------------------- class TestGetLogVerbosity: def test_returns_200(self, client): mock_cm = MagicMock() mock_cm.get_logging_config.return_value = { 'python': {'root': 'INFO', 'services': {'network': 'INFO'}}, 'containers': {'caddy': 'INFO'}, } with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/logs/verbosity') assert resp.status_code == 200 def test_returns_python_and_container_sections(self, client): mock_cm = MagicMock() mock_cm.get_logging_config.return_value = { 'python': {'root': 'INFO', 'services': {'network': 'DEBUG', 'email': 'INFO'}}, 'containers': {'caddy': 'WARNING'}, } with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/logs/verbosity') data = json.loads(resp.data) assert data['python']['services']['network'] == 'DEBUG' assert data['containers']['caddy'] == 'WARNING' def test_500_on_exception(self, client): mock_cm = MagicMock() mock_cm.get_logging_config.side_effect = Exception('fail') with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/logs/verbosity') assert resp.status_code == 500 # --------------------------------------------------------------------------- # PUT /api/logs/verbosity # --------------------------------------------------------------------------- class TestSetLogVerbosity: def test_returns_200(self, client): mock_cm = MagicMock() mock_cm.get_logging_config.return_value = { 'python': {'root': 'INFO', 'services': {'network': 'DEBUG'}}, 'containers': {}, } with patch.object(app_module, 'config_manager', mock_cm), \ patch.object(app_module, 'log_manager', MagicMock()): resp = client.put('/api/logs/verbosity', data=json.dumps({'python': {'services': {'network': 'DEBUG'}}}), content_type='application/json') assert resp.status_code == 200 def test_persists_via_config_manager_and_applies_hot(self, client): mock_cm = MagicMock() mock_cm.get_logging_config.return_value = {'python': {}, 'containers': {}} mock_lm = MagicMock() with patch.object(app_module, 'config_manager', mock_cm), \ patch.object(app_module, 'log_manager', mock_lm): client.put('/api/logs/verbosity', data=json.dumps({'python': {'services': {'network': 'DEBUG'}}}), content_type='application/json') mock_cm.set_python_log_level.assert_called_with('network', 'DEBUG') mock_lm.set_service_level.assert_called_with('network', 'DEBUG') def test_500_on_exception(self, client): mock_cm = MagicMock() mock_cm.set_python_log_level.side_effect = Exception('fail') with patch.object(app_module, 'config_manager', mock_cm), \ patch.object(app_module, 'log_manager', MagicMock()): resp = client.put('/api/logs/verbosity', data=json.dumps({'python': {'services': {'network': 'DEBUG'}}}), content_type='application/json') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/active # --------------------------------------------------------------------------- class TestGetActiveServices: def test_returns_200(self, client): reg = _make_registry([_make_service('email')]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/active') assert resp.status_code == 200 def test_returns_active_services_list(self, client): reg = _make_registry([ {'id': 'email', 'name': 'Email', 'subdomain': 'mail', 'capabilities': {'smtp': True}}, ]) with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/active') data = json.loads(resp.data) assert isinstance(data, list) assert len(data) == 1 assert data[0]['id'] == 'email' def test_500_on_exception(self, client): reg = MagicMock() reg.list_active.side_effect = Exception('error') with patch.object(app_module, 'service_registry', reg): resp = client.get('/api/services/active') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/status # --------------------------------------------------------------------------- class TestGetAllServicesStatus: def test_returns_200(self, client): mock_sbus = MagicMock() mock_sbus.list_services.return_value = [] with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/status') assert resp.status_code == 200 def test_returns_status_for_each_service(self, client): mock_svc = MagicMock() mock_svc.get_status.return_value = { 'status': 'online', 'running': True, 'timestamp': '2026-01-01T00:00:00' } mock_sbus = MagicMock() mock_sbus.list_services.return_value = ['network'] mock_sbus.get_service.return_value = mock_svc with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/status') data = json.loads(resp.data) assert 'network' in data def test_service_exception_sets_error_in_status(self, client): mock_svc = MagicMock() mock_svc.get_status.side_effect = Exception('service down') mock_sbus = MagicMock() mock_sbus.list_services.return_value = ['email'] mock_sbus.get_service.return_value = mock_svc with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/status') data = json.loads(resp.data) assert 'email' in data assert resp.status_code == 200 # --------------------------------------------------------------------------- # GET /api/services/connectivity # --------------------------------------------------------------------------- class TestTestAllServicesConnectivity: def test_returns_200(self, client): mock_sbus = MagicMock() mock_sbus.list_services.return_value = [] with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/connectivity') assert resp.status_code == 200 def test_includes_connectivity_result(self, client): mock_svc = MagicMock() mock_svc.test_connectivity.return_value = {'success': True} mock_sbus = MagicMock() mock_sbus.list_services.return_value = ['network'] mock_sbus.get_service.return_value = mock_svc with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/connectivity') data = json.loads(resp.data) assert 'network' in data def test_500_on_bus_exception(self, client): mock_sbus = MagicMock() mock_sbus.list_services.side_effect = Exception('bus fail') with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/connectivity') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/logs (backend logs endpoint) # --------------------------------------------------------------------------- class TestGetBackendLogs: def test_returns_404_when_log_file_missing(self, client): # The default state: picell.log does not exist in the api/ directory import os import routes.services as svc_routes # If the log file doesn't exist, it should return 404 original = os.path.exists def fake_exists(path): if 'picell.log' in path: return False return original(path) with patch('routes.services.os.path.exists', side_effect=fake_exists): resp = client.get('/api/logs') assert resp.status_code == 404 # --------------------------------------------------------------------------- # POST /api/logs/export # --------------------------------------------------------------------------- class TestExportLogsEndpoint: def test_returns_200(self, client): mock_lm = MagicMock() mock_lm.export_logs.return_value = '[]' with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/export', data=json.dumps({'format': 'json'}), content_type='application/json') assert resp.status_code == 200 def test_returns_logs_and_format(self, client): mock_lm = MagicMock() mock_lm.export_logs.return_value = '[{"level":"INFO"}]' with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/export', data=json.dumps({'format': 'json'}), content_type='application/json') data = json.loads(resp.data) assert 'logs' in data assert data['format'] == 'json' def test_500_on_exception(self, client): mock_lm = MagicMock() mock_lm.export_logs.side_effect = Exception('export failed') with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/export', data=json.dumps({'format': 'json'}), content_type='application/json') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/services/status — service-specific branches (wireguard, email, etc.) # --------------------------------------------------------------------------- class TestGetAllServicesStatusServiceBranches: """Cover the elif branches in get_all_services_status for each service type.""" def _status_for_service(self, client, service_name, raw_status): mock_svc = MagicMock() mock_svc.get_status.return_value = dict( {'status': 'online', 'running': True, 'timestamp': '2026-01-01T00:00:00'}, **raw_status ) mock_sbus = MagicMock() mock_sbus.list_services.return_value = [service_name] mock_sbus.get_service.return_value = mock_svc with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/status') return json.loads(resp.data) def test_wireguard_branch_includes_peers_count(self, client): data = self._status_for_service(client, 'wireguard', {'peers_count': 3, 'interface': 'wg0'}) assert data['wireguard'].get('peers_count') == 3 assert data['wireguard'].get('interface') == 'wg0' def test_email_branch_includes_users_count(self, client): data = self._status_for_service(client, 'email', {'users_count': 5, 'domain': 'cell.local'}) assert data['email'].get('users_count') == 5 assert data['email'].get('domain') == 'cell.local' def test_calendar_branch_includes_calendars_count(self, client): data = self._status_for_service(client, 'calendar', {'users_count': 2, 'calendars_count': 4}) assert data['calendar'].get('calendars_count') == 4 def test_files_branch_includes_storage_used(self, client): data = self._status_for_service(client, 'files', {'users_count': 1, 'total_storage_used': {'used': 100}}) assert data['files'].get('storage_used') == {'used': 100} def test_routing_branch_includes_nat_rules_count(self, client): data = self._status_for_service(client, 'routing', {'nat_rules_count': 2, 'peer_routes_count': 1, 'firewall_rules_count': 3}) assert data['routing'].get('nat_rules_count') == 2 def test_vault_branch_includes_certificates_count(self, client): data = self._status_for_service(client, 'vault', {'certificates_count': 5, 'trusted_keys_count': 2}) assert data['vault'].get('certificates_count') == 5 def test_non_dict_status_uses_string_fallback(self, client): """When get_status returns a non-dict, it gets stored as a string+bool.""" mock_svc = MagicMock() mock_svc.get_status.return_value = 'running' mock_sbus = MagicMock() mock_sbus.list_services.return_value = ['network'] mock_sbus.get_service.return_value = mock_svc with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/status') data = json.loads(resp.data) assert 'network' in data # --------------------------------------------------------------------------- # Error paths in catalog service management endpoints # --------------------------------------------------------------------------- class TestServiceCatalogErrorPaths: """Cover ValueError/Exception branches in catalog management endpoints.""" def test_get_service_container_status_value_error_returns_400(self, client): mock_reg = MagicMock() mock_reg.get.return_value = {'id': 'email', 'kind': 'builtin'} mock_composer = MagicMock() mock_composer.status_service.side_effect = ValueError('bad request') with patch.object(app_module, 'service_registry', mock_reg), \ patch.object(app_module, 'service_composer', mock_composer): resp = client.get('/api/services/catalog/email/status') assert resp.status_code == 400 def test_restart_service_containers_value_error_returns_400(self, client): mock_reg = MagicMock() mock_reg.get.return_value = {'id': 'email', 'kind': 'builtin'} mock_composer = MagicMock() mock_composer.restart_service.side_effect = ValueError('bad service') with patch.object(app_module, 'service_registry', mock_reg), \ patch.object(app_module, 'service_composer', mock_composer): resp = client.post('/api/services/catalog/email/restart') assert resp.status_code == 400 def test_restart_service_containers_exception_returns_500(self, client): mock_reg = MagicMock() mock_reg.get.return_value = {'id': 'email', 'kind': 'builtin'} mock_composer = MagicMock() mock_composer.restart_service.side_effect = Exception('docker down') with patch.object(app_module, 'service_registry', mock_reg), \ patch.object(app_module, 'service_composer', mock_composer): resp = client.post('/api/services/catalog/email/restart') assert resp.status_code == 500 def test_reconfigure_service_value_error_returns_400(self, client): mock_reg = MagicMock() mock_reg.get.return_value = {'id': 'myapp', 'kind': 'store'} mock_composer = MagicMock() mock_composer.has_compose_file.return_value = True mock_composer.up.side_effect = ValueError('invalid config') with patch.object(app_module, 'service_registry', mock_reg), \ patch.object(app_module, 'service_composer', mock_composer): resp = client.post('/api/services/catalog/myapp/reconfigure') assert resp.status_code == 400 def test_reconfigure_service_exception_returns_500(self, client): mock_reg = MagicMock() mock_reg.get.return_value = {'id': 'myapp', 'kind': 'store'} mock_composer = MagicMock() mock_composer.has_compose_file.return_value = True mock_composer.up.side_effect = Exception('compose fail') with patch.object(app_module, 'service_registry', mock_reg), \ patch.object(app_module, 'service_composer', mock_composer): resp = client.post('/api/services/catalog/myapp/reconfigure') assert resp.status_code == 500 def test_provision_service_account_exception_returns_500(self, client): mock_am = MagicMock() mock_am.provision.side_effect = Exception('db failure') with patch.object(app_module, 'account_manager', mock_am): resp = client.post('/api/services/catalog/email/accounts', data=json.dumps({'username': 'alice'}), content_type='application/json') assert resp.status_code == 500 def test_deprovision_service_account_value_error_returns_400(self, client): mock_am = MagicMock() mock_am.deprovision.side_effect = ValueError('user not found') with patch.object(app_module, 'account_manager', mock_am): resp = client.delete('/api/services/catalog/email/accounts/alice') assert resp.status_code == 400 def test_deprovision_service_account_exception_returns_500(self, client): mock_am = MagicMock() mock_am.deprovision.side_effect = Exception('db down') with patch.object(app_module, 'account_manager', mock_am): resp = client.delete('/api/services/catalog/email/accounts/alice') assert resp.status_code == 500 def test_get_service_account_credentials_exception_returns_500(self, client): mock_am = MagicMock() mock_am.get_credentials.side_effect = Exception('db fail') with patch.object(app_module, 'account_manager', mock_am): resp = client.get('/api/services/catalog/email/accounts/alice/credentials') assert resp.status_code == 500 # --------------------------------------------------------------------------- # Exception paths for bus event/start/stop and log endpoints # --------------------------------------------------------------------------- class TestServiceBusEndpointExceptions: """Cover exception paths for service bus and log endpoints.""" def test_get_service_bus_events_exception_returns_500(self, client): mock_sbus = MagicMock() mock_sbus.get_event_history.side_effect = Exception('bus crash') with patch.object(app_module, 'service_bus', mock_sbus): resp = client.get('/api/services/bus/events') assert resp.status_code == 500 def test_start_service_exception_returns_500(self, client): mock_sbus = MagicMock() mock_sbus.orchestrate_service_start.side_effect = Exception('crash') with patch.object(app_module, 'service_bus', mock_sbus): resp = client.post('/api/services/bus/services/email/start') assert resp.status_code == 500 def test_stop_service_exception_returns_500(self, client): mock_sbus = MagicMock() mock_sbus.orchestrate_service_stop.side_effect = Exception('crash') with patch.object(app_module, 'service_bus', mock_sbus): resp = client.post('/api/services/bus/services/email/stop') assert resp.status_code == 500 def test_search_logs_exception_returns_500(self, client): mock_lm = MagicMock() mock_lm.search_logs.side_effect = Exception('search fail') with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/search', data='{}', content_type='application/json') assert resp.status_code == 500 def test_get_log_statistics_exception_returns_500(self, client): mock_lm = MagicMock() mock_lm.get_log_statistics.side_effect = Exception('stats fail') with patch.object(app_module, 'log_manager', mock_lm): resp = client.get('/api/logs/statistics') assert resp.status_code == 500 def test_rotate_logs_exception_returns_500(self, client): mock_lm = MagicMock() mock_lm.rotate_logs.side_effect = Exception('rotate fail') with patch.object(app_module, 'log_manager', mock_lm): resp = client.post('/api/logs/rotate', data='{}', content_type='application/json') assert resp.status_code == 500