#!/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