test: raise coverage 68.7% -> ~80.4%; add ~250 tests for new egress/DDNS/network paths
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>
This commit is contained in:
2026-06-10 09:03:39 -04:00
parent c41cadafb4
commit aa1e5c41ec
33 changed files with 9446 additions and 631 deletions
+268
View File
@@ -0,0 +1,268 @@
"""
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