feat: remove optional services step from setup wizard
Services are now installed post-setup from the Store page, so the wizard step that let users pre-select email/calendar/files is removed. Reduces wizard from 5 steps to 4 (Step4Services deleted, Step5Review renamed to Step4Review). Backend drops services_enabled validation, background install thread, and service_store_manager dependency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,6 @@ def _valid_payload(**overrides):
|
||||
'password': 'SecurePass1!',
|
||||
'domain_mode': 'lan',
|
||||
'timezone': 'UTC',
|
||||
'services_enabled': ['wireguard'],
|
||||
'ddns_provider': 'none',
|
||||
}
|
||||
base.update(overrides)
|
||||
@@ -203,7 +202,7 @@ def test_complete_setup_calls_set_identity_field_for_each_field(
|
||||
setup_manager.complete_setup(_valid_payload())
|
||||
calls = mock_config_manager.set_identity_field.call_args_list
|
||||
field_names = [c[0][0] for c in calls]
|
||||
for expected in ('cell_name', 'domain_mode', 'timezone', 'services_enabled', 'ddns_provider'):
|
||||
for expected in ('cell_name', 'domain_mode', 'timezone', 'ddns_provider'):
|
||||
assert expected in field_names, f"set_identity_field not called for '{expected}'"
|
||||
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
"""Tests for SetupManager service-install wiring added in Phase 6.
|
||||
|
||||
Verifies that complete_setup() triggers background service installs when
|
||||
service_store_manager is provided and services_enabled is non-empty.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'api'))
|
||||
from setup_manager import SetupManager
|
||||
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _valid_payload(**overrides):
|
||||
base = {
|
||||
'cell_name': 'mycel',
|
||||
'password': 'SecurePass1!',
|
||||
'domain_mode': 'lan',
|
||||
'timezone': 'UTC',
|
||||
'services_enabled': ['email'],
|
||||
'ddns_provider': 'none',
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def _make_setup_manager(services_enabled=None, service_store_manager=None):
|
||||
config_mgr = MagicMock()
|
||||
config_mgr.get_identity.return_value = {}
|
||||
auth_mgr = MagicMock()
|
||||
auth_mgr.create_user.return_value = True
|
||||
return SetupManager(
|
||||
config_manager=config_mgr,
|
||||
auth_manager=auth_mgr,
|
||||
service_store_manager=service_store_manager,
|
||||
), config_mgr, auth_mgr
|
||||
|
||||
|
||||
# ── constructor ───────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSetupManagerConstructor(unittest.TestCase):
|
||||
|
||||
def test_accepts_service_store_manager_kwarg(self):
|
||||
ssm = MagicMock()
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
self.assertIs(sm.service_store_manager, ssm)
|
||||
|
||||
def test_defaults_service_store_manager_to_none(self):
|
||||
config_mgr = MagicMock()
|
||||
config_mgr.get_identity.return_value = {}
|
||||
auth_mgr = MagicMock()
|
||||
sm = SetupManager(config_manager=config_mgr, auth_manager=auth_mgr)
|
||||
self.assertIsNone(sm.service_store_manager)
|
||||
|
||||
|
||||
# ── complete_setup + service install wiring ───────────────────────────────────
|
||||
|
||||
class TestCompleteSetupInstallWiring(unittest.TestCase):
|
||||
|
||||
def test_no_service_store_manager_does_not_crash(self, tmp_path=None):
|
||||
"""service_store_manager=None with services_enabled set must not raise."""
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=None)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=['email']))
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
def test_empty_services_enabled_no_install_called(self):
|
||||
"""install() must not be called when services_enabled is empty."""
|
||||
ssm = MagicMock()
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=[]))
|
||||
time.sleep(0.05)
|
||||
ssm.install.assert_not_called()
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
def test_services_enabled_triggers_install_in_background(self):
|
||||
"""install() must be called with each service id after complete_setup."""
|
||||
ssm = MagicMock()
|
||||
ssm.install.return_value = {'ok': True}
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=['email']))
|
||||
self.assertTrue(result['success'])
|
||||
# Give the daemon thread time to finish
|
||||
time.sleep(0.1)
|
||||
ssm.install.assert_called_with('email')
|
||||
|
||||
def test_multiple_services_all_installed(self):
|
||||
ssm = MagicMock()
|
||||
ssm.install.return_value = {'ok': True}
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(
|
||||
_valid_payload(services_enabled=['email', 'calendar', 'files'])
|
||||
)
|
||||
self.assertTrue(result['success'])
|
||||
time.sleep(0.1)
|
||||
installed = [c[0][0] for c in ssm.install.call_args_list]
|
||||
self.assertIn('email', installed)
|
||||
self.assertIn('calendar', installed)
|
||||
self.assertIn('files', installed)
|
||||
|
||||
def test_install_failure_does_not_fail_wizard(self):
|
||||
"""An exception from install() must not propagate to complete_setup."""
|
||||
ssm = MagicMock()
|
||||
ssm.install.side_effect = RuntimeError('Docker daemon unreachable')
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=['email']))
|
||||
self.assertTrue(result['success'])
|
||||
self.assertEqual(result.get('redirect'), '/login')
|
||||
|
||||
def test_install_returning_error_dict_does_not_fail_wizard(self):
|
||||
"""install() returning {'ok': False} must not affect setup result."""
|
||||
ssm = MagicMock()
|
||||
ssm.install.return_value = {'ok': False, 'error': 'image pull failed'}
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=['email']))
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
def test_complete_setup_still_marks_setup_complete_before_install(self):
|
||||
"""setup_complete must be persisted even if install thread races."""
|
||||
ssm = MagicMock()
|
||||
ssm.install.return_value = {'ok': True}
|
||||
sm, config_mgr, _ = _make_setup_manager(service_store_manager=ssm)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
sm.complete_setup(_valid_payload(services_enabled=['email']))
|
||||
# setup_complete must be the last set_identity_field call
|
||||
calls = config_mgr.set_identity_field.call_args_list
|
||||
last = calls[-1]
|
||||
self.assertEqual(last, call('setup_complete', True))
|
||||
|
||||
def test_service_store_manager_none_with_empty_services_succeeds(self):
|
||||
sm, _, _ = _make_setup_manager(service_store_manager=None)
|
||||
with patch.dict(os.environ, {'DATA_DIR': self._tmp()}):
|
||||
result = sm.complete_setup(_valid_payload(services_enabled=[]))
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _tmp(self):
|
||||
"""Return a writable temp dir path string for DATA_DIR."""
|
||||
import tempfile
|
||||
return tempfile.mkdtemp()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user