diff --git a/api/routes/setup.py b/api/routes/setup.py index fa2afb8..de14975 100644 --- a/api/routes/setup.py +++ b/api/routes/setup.py @@ -90,15 +90,19 @@ def complete_setup(): result = sm.complete_setup(payload) if result.get('success'): try: - from app import config_manager, service_bus, EventType + from app import config_manager, service_bus, EventType, network_manager identity = config_manager.configs.get('_identity', {}) + cell_name = identity.get('cell_name', '') service_bus.publish_event(EventType.IDENTITY_CHANGED, 'setup', { - 'cell_name': identity.get('cell_name'), + 'cell_name': cell_name, 'domain': identity.get('domain'), 'domain_name': identity.get('domain_name'), 'domain_mode': identity.get('domain_mode'), 'effective_domain': config_manager.get_effective_domain(), }) + # Bootstrap wrote the zone with 'mycell'; rename to the real cell name. + if cell_name: + network_manager.apply_cell_name('', cell_name) except Exception as exc: logger.warning(f'Failed to publish IDENTITY_CHANGED after setup: {exc}') status_code = 200 if result.get('success') else 400 diff --git a/tests/test_setup_route.py b/tests/test_setup_route.py new file mode 100644 index 0000000..863aa1a --- /dev/null +++ b/tests/test_setup_route.py @@ -0,0 +1,133 @@ +#!/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'