""" Tests for routes/service_store.py: - GET /api/store/services - GET /api/store/services//manifest - POST /api/store/services//install - DELETE /api/store/services/ - GET /api/store/installed - POST /api/store/refresh """ 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 # --------------------------------------------------------------------------- # GET /api/store/services # --------------------------------------------------------------------------- class TestListStoreServices: def test_returns_200(self, client): mock_ssm = MagicMock() mock_ssm.list_services.return_value = {'available': [], 'installed': []} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.get('/api/store/services') assert resp.status_code == 200 def test_returns_service_index(self, client): mock_ssm = MagicMock() mock_ssm.list_services.return_value = { 'available': [{'id': 'nextcloud', 'name': 'Nextcloud'}], 'installed': [] } with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.get('/api/store/services') data = json.loads(resp.data) assert 'available' in data assert len(data['available']) == 1 def test_500_on_exception(self, client): mock_ssm = MagicMock() mock_ssm.list_services.side_effect = Exception('network error') with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.get('/api/store/services') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/store/services//manifest # --------------------------------------------------------------------------- class TestGetManifest: def test_returns_200_on_success(self, client): import requests as _requests mock_resp = MagicMock() mock_resp.json.return_value = {'id': 'nextcloud', 'version': '1.0'} mock_resp.raise_for_status.return_value = None with patch('routes.service_store._requests') as mock_req: mock_req.get.return_value = mock_resp mock_req.HTTPError = _requests.HTTPError resp = client.get('/api/store/services/nextcloud/manifest') assert resp.status_code == 200 def test_returns_manifest_data(self, client): import requests as _requests mock_resp = MagicMock() mock_resp.json.return_value = {'id': 'nextcloud', 'version': '1.0', 'name': 'Nextcloud'} mock_resp.raise_for_status.return_value = None with patch('routes.service_store._requests') as mock_req: mock_req.get.return_value = mock_resp mock_req.HTTPError = _requests.HTTPError resp = client.get('/api/store/services/nextcloud/manifest') data = json.loads(resp.data) assert data['id'] == 'nextcloud' def test_returns_404_on_http_error(self, client): import requests as _requests with patch('routes.service_store._requests') as mock_req: mock_req.HTTPError = _requests.HTTPError mock_req.get.return_value = MagicMock( raise_for_status=MagicMock( side_effect=_requests.HTTPError('404 Not Found'))) resp = client.get('/api/store/services/unknown/manifest') assert resp.status_code == 404 def test_500_on_network_error(self, client): import requests as _requests with patch('routes.service_store._requests') as mock_req: mock_req.HTTPError = _requests.HTTPError mock_req.get.side_effect = Exception('network timeout') resp = client.get('/api/store/services/nextcloud/manifest') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/store/services//install # --------------------------------------------------------------------------- class TestInstallService: def test_returns_200_on_success(self, client): mock_ssm = MagicMock() mock_ssm.install.return_value = {'ok': True, 'message': 'Installed'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/services/nextcloud/install', content_type='application/json') assert resp.status_code == 200 def test_returns_install_result(self, client): mock_ssm = MagicMock() mock_ssm.install.return_value = {'ok': True, 'message': 'Installed'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/services/nextcloud/install', content_type='application/json') data = json.loads(resp.data) assert data['ok'] is True def test_returns_400_on_failure(self, client): mock_ssm = MagicMock() mock_ssm.install.return_value = {'ok': False, 'error': 'Manifest not found'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/services/nextcloud/install', content_type='application/json') assert resp.status_code == 400 def test_normalizes_stderr_to_error_key(self, client): """When ok=False but only stderr is set, it becomes the error key.""" mock_ssm = MagicMock() mock_ssm.install.return_value = {'ok': False, 'stderr': 'docker pull failed'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/services/nextcloud/install', content_type='application/json') data = json.loads(resp.data) assert data.get('error') == 'docker pull failed' assert resp.status_code == 400 def test_500_on_exception(self, client): mock_ssm = MagicMock() mock_ssm.install.side_effect = Exception('unexpected error') with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/services/nextcloud/install', content_type='application/json') assert resp.status_code == 500 # --------------------------------------------------------------------------- # DELETE /api/store/services/ # --------------------------------------------------------------------------- class TestRemoveService: def test_returns_200_on_success(self, client): mock_ssm = MagicMock() mock_ssm.remove.return_value = {'ok': True, 'message': 'Removed'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.delete('/api/store/services/nextcloud') assert resp.status_code == 200 def test_returns_404_when_not_installed(self, client): mock_ssm = MagicMock() mock_ssm.remove.return_value = {'ok': False, 'error': 'not installed'} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.delete('/api/store/services/nextcloud') assert resp.status_code == 404 def test_passes_purge_flag(self, client): mock_ssm = MagicMock() mock_ssm.remove.return_value = {'ok': True} with patch.object(app_module, 'service_store_manager', mock_ssm): client.delete('/api/store/services/nextcloud?purge=true') mock_ssm.remove.assert_called_once_with('nextcloud', purge_data=True) def test_purge_false_by_default(self, client): mock_ssm = MagicMock() mock_ssm.remove.return_value = {'ok': True} with patch.object(app_module, 'service_store_manager', mock_ssm): client.delete('/api/store/services/nextcloud') mock_ssm.remove.assert_called_once_with('nextcloud', purge_data=False) def test_500_on_exception(self, client): mock_ssm = MagicMock() mock_ssm.remove.side_effect = Exception('docker error') with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.delete('/api/store/services/nextcloud') assert resp.status_code == 500 # --------------------------------------------------------------------------- # GET /api/store/installed # --------------------------------------------------------------------------- class TestGetInstalled: def test_returns_200(self, client): mock_cm = MagicMock() mock_cm.get_installed_services.return_value = ['nextcloud'] with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/store/installed') assert resp.status_code == 200 def test_returns_installed_list(self, client): mock_cm = MagicMock() mock_cm.get_installed_services.return_value = ['nextcloud', 'gitea'] with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/store/installed') data = json.loads(resp.data) assert 'installed' in data assert 'nextcloud' in data['installed'] assert 'gitea' in data['installed'] def test_returns_empty_when_nothing_installed(self, client): mock_cm = MagicMock() mock_cm.get_installed_services.return_value = [] with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/store/installed') data = json.loads(resp.data) assert data['installed'] == [] def test_500_on_exception(self, client): mock_cm = MagicMock() mock_cm.get_installed_services.side_effect = Exception('config error') with patch.object(app_module, 'config_manager', mock_cm): resp = client.get('/api/store/installed') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/store/refresh # --------------------------------------------------------------------------- class TestRefreshIndex: def test_returns_200(self, client): mock_ssm = MagicMock() mock_ssm._index_cache = {'data': 'old'} mock_ssm._index_cache_time = 12345 mock_ssm.list_services.return_value = {'available': [], 'installed': []} with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/refresh', content_type='application/json') assert resp.status_code == 200 def test_clears_cache(self, client): mock_ssm = MagicMock() mock_ssm._index_cache = {'data': 'old'} mock_ssm._index_cache_time = 12345 mock_ssm.list_services.return_value = {} with patch.object(app_module, 'service_store_manager', mock_ssm): client.post('/api/store/refresh', content_type='application/json') assert mock_ssm._index_cache is None assert mock_ssm._index_cache_time == 0 def test_500_on_exception(self, client): mock_ssm = MagicMock() mock_ssm.list_services.side_effect = Exception('cache error') with patch.object(app_module, 'service_store_manager', mock_ssm): resp = client.post('/api/store/refresh', content_type='application/json') assert resp.status_code == 500