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>
269 lines
11 KiB
Python
269 lines
11 KiB
Python
"""
|
|
Tests for routes/service_store.py:
|
|
- GET /api/store/services
|
|
- GET /api/store/services/<id>/manifest
|
|
- POST /api/store/services/<id>/install
|
|
- DELETE /api/store/services/<id>
|
|
- 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/<id>/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/<id>/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/<id>
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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
|