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>
339 lines
12 KiB
Python
339 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for the /api/setup/complete route in routes/setup.py.
|
|
|
|
Verifies that completing the wizard fires IDENTITY_CHANGED AND updates the
|
|
primary DNS zone with the real cell name (fixing the 'mycell' placeholder
|
|
written by _bootstrap_dns before the wizard runs).
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, call
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / 'api'))
|
|
|
|
from app import app
|
|
|
|
|
|
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
|
|
def _valid_payload(**overrides):
|
|
base = {
|
|
'cell_name': 'mycel',
|
|
'password': 'SecurePass1!',
|
|
'domain_mode': 'lan',
|
|
'timezone': 'UTC',
|
|
'ddns_provider': 'none',
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
@pytest.fixture()
|
|
def client():
|
|
app.config['TESTING'] = True
|
|
app.config['SECRET_KEY'] = 'test-secret'
|
|
with app.test_client() as c:
|
|
yield c
|
|
|
|
|
|
def _post_complete(client, payload):
|
|
return client.post(
|
|
'/api/setup/complete',
|
|
data=json.dumps(payload),
|
|
content_type='application/json',
|
|
)
|
|
|
|
|
|
# ── tests ──────────────────────────────────────────────────────────────────────
|
|
|
|
def test_complete_setup_calls_apply_cell_name_on_success(client, tmp_path):
|
|
"""After successful wizard, DNS zone is updated with the real cell name."""
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.complete_setup.return_value = {'success': True, 'redirect': '/login'}
|
|
|
|
mock_cm = MagicMock()
|
|
mock_cm.configs = {'_identity': {'cell_name': 'mycel'}}
|
|
mock_cm.get_effective_domain.return_value = 'mycel.pic.ngo'
|
|
|
|
mock_nm = MagicMock()
|
|
mock_sbus = MagicMock()
|
|
|
|
with (
|
|
patch('app.setup_manager', mock_sm),
|
|
patch('app.config_manager', mock_cm),
|
|
patch('app.network_manager', mock_nm),
|
|
patch('app.service_bus', mock_sbus),
|
|
patch.dict('sys.modules', {'app': __import__('app')}),
|
|
):
|
|
resp = _post_complete(client, _valid_payload())
|
|
|
|
assert resp.status_code == 200
|
|
mock_nm.apply_cell_name.assert_called_once_with('', 'mycel')
|
|
|
|
|
|
def test_complete_setup_does_not_call_apply_cell_name_on_failure(client, tmp_path):
|
|
"""DNS zone is NOT touched when setup fails."""
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.complete_setup.return_value = {'success': False, 'errors': ['bad']}
|
|
|
|
mock_nm = MagicMock()
|
|
|
|
with (
|
|
patch('app.setup_manager', mock_sm),
|
|
patch('app.network_manager', mock_nm),
|
|
):
|
|
resp = _post_complete(client, _valid_payload())
|
|
|
|
assert resp.status_code == 400
|
|
mock_nm.apply_cell_name.assert_not_called()
|
|
|
|
|
|
def test_complete_setup_returns_410_when_already_complete(client):
|
|
"""Route returns 410 if setup was already done."""
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = True
|
|
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_complete(client, _valid_payload())
|
|
|
|
assert resp.status_code == 410
|
|
|
|
|
|
def test_complete_setup_fires_identity_changed_on_success(client):
|
|
"""IDENTITY_CHANGED is published after successful wizard completion."""
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.complete_setup.return_value = {'success': True, 'redirect': '/login'}
|
|
|
|
mock_cm = MagicMock()
|
|
mock_cm.configs = {'_identity': {'cell_name': 'mycel', 'domain': 'cell'}}
|
|
mock_cm.get_effective_domain.return_value = 'cell'
|
|
|
|
mock_sbus = MagicMock()
|
|
mock_nm = MagicMock()
|
|
|
|
with (
|
|
patch('app.setup_manager', mock_sm),
|
|
patch('app.config_manager', mock_cm),
|
|
patch('app.network_manager', mock_nm),
|
|
patch('app.service_bus', mock_sbus),
|
|
):
|
|
resp = _post_complete(client, _valid_payload())
|
|
|
|
assert resp.status_code == 200
|
|
mock_sbus.publish_event.assert_called_once()
|
|
event_args = mock_sbus.publish_event.call_args
|
|
assert event_args[0][1] == 'setup'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/setup/status
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_get_setup_status_returns_200_when_incomplete(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.get_setup_status.return_value = {'step': 'cell_name', 'complete': False}
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = client.get('/api/setup/status')
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_get_setup_status_returns_410_when_already_complete(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = True
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = client.get('/api/setup/status')
|
|
assert resp.status_code == 410
|
|
|
|
|
|
def test_get_setup_status_returns_setup_data(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.get_setup_status.return_value = {'step': 'cell_name', 'options': {}}
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = client.get('/api/setup/status')
|
|
data = json.loads(resp.data)
|
|
assert 'step' in data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /api/setup/validate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _post_validate(client, payload):
|
|
return client.post(
|
|
'/api/setup/validate',
|
|
data=json.dumps(payload),
|
|
content_type='application/json',
|
|
)
|
|
|
|
|
|
def test_validate_cell_name_valid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = []
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'cell_name', 'data': {'cell_name': 'mycel'}})
|
|
assert resp.status_code == 200
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is True
|
|
assert data['errors'] == []
|
|
|
|
|
|
def test_validate_cell_name_invalid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = ['Name too short']
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'cell_name', 'data': {'cell_name': 'a'}})
|
|
assert resp.status_code == 200
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is False
|
|
assert len(data['errors']) > 0
|
|
|
|
|
|
def test_validate_password_valid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_password.return_value = []
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'password', 'data': {'password': 'StrongPass1!'}})
|
|
assert resp.status_code == 200
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is True
|
|
|
|
|
|
def test_validate_password_invalid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_password.return_value = ['Too short']
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'password', 'data': {'password': 'weak'}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is False
|
|
|
|
|
|
def test_validate_pic_ngo_available_when_available(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = []
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._check_pic_ngo_available', return_value=True):
|
|
resp = _post_validate(client, {
|
|
'step': 'pic_ngo_available', 'data': {'cell_name': 'mycel'}})
|
|
data = json.loads(resp.data)
|
|
assert data['available'] is True
|
|
|
|
|
|
def test_validate_pic_ngo_available_when_taken(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = []
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._check_pic_ngo_available', return_value=False):
|
|
resp = _post_validate(client, {
|
|
'step': 'pic_ngo_available', 'data': {'cell_name': 'mycel'}})
|
|
data = json.loads(resp.data)
|
|
assert data['available'] is False
|
|
|
|
|
|
def test_validate_pic_ngo_name_errors_block_check(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = ['Invalid name']
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {
|
|
'step': 'pic_ngo_available', 'data': {'cell_name': 'a'}})
|
|
data = json.loads(resp.data)
|
|
assert data['available'] is False
|
|
|
|
|
|
def test_validate_pic_ngo_service_unreachable_returns_503(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
mock_sm.validate_cell_name.return_value = []
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._check_pic_ngo_available', side_effect=Exception('timeout')):
|
|
resp = _post_validate(client, {
|
|
'step': 'pic_ngo_available', 'data': {'cell_name': 'mycel'}})
|
|
assert resp.status_code == 503
|
|
data = json.loads(resp.data)
|
|
assert data['available'] is False
|
|
|
|
|
|
def test_validate_cloudflare_token_valid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._verify_cloudflare_token', return_value=True):
|
|
resp = _post_validate(client, {
|
|
'step': 'cloudflare_token', 'data': {'token': 'mytoken'}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is True
|
|
|
|
|
|
def test_validate_cloudflare_token_missing(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {
|
|
'step': 'cloudflare_token', 'data': {'token': ''}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is False
|
|
|
|
|
|
def test_validate_cloudflare_token_invalid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._verify_cloudflare_token', return_value=False):
|
|
resp = _post_validate(client, {
|
|
'step': 'cloudflare_token', 'data': {'token': 'badtoken'}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is False
|
|
|
|
|
|
def test_validate_duckdns_token_valid(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm), \
|
|
patch('routes.setup._verify_duckdns_token', return_value=True):
|
|
resp = _post_validate(client, {
|
|
'step': 'duckdns_token', 'data': {'subdomain': 'mycel', 'token': 'abc'}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is True
|
|
|
|
|
|
def test_validate_duckdns_token_missing_fields(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {
|
|
'step': 'duckdns_token', 'data': {'subdomain': '', 'token': ''}})
|
|
data = json.loads(resp.data)
|
|
assert data['valid'] is False
|
|
|
|
|
|
def test_validate_unknown_step_returns_400(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = False
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'unknown_step', 'data': {}})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_validate_returns_410_when_setup_complete(client):
|
|
mock_sm = MagicMock()
|
|
mock_sm.is_setup_complete.return_value = True
|
|
with patch('app.setup_manager', mock_sm):
|
|
resp = _post_validate(client, {'step': 'cell_name', 'data': {'cell_name': 'x'}})
|
|
assert resp.status_code == 410
|