Make private desktop tools opt-in and preserve host defaults
This commit is contained in:
1 parent
0ae1fccfba
commit
e84362b999
15 files changed
+219
-45
No files matched your search
@@ -86,7 +86,10 @@ class HostCredentialAutostartTests(unittest.TestCase):
|
||||
def test_node_255_startup_does_not_need_a_credential_backend(self):
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
self.helper.side_effect = RuntimeError("missing helper")
|
||||
autostart._enable(self.state, confirm=False, start_now=False)
|
||||
# The absent-helper case must not depend on another package being
|
||||
# installed on the machine running this source-checkout test.
|
||||
with mock.patch.object(autostart, "_helper_present", return_value=False):
|
||||
autostart._enable(self.state, confirm=False, start_now=False)
|
||||
self.helper.assert_not_called(); self.prompt.assert_not_called()
|
||||
self.assertFalse(self.operations("protect"))
|
||||
self.assertFalse(any(args[:2] == ["systemd-creds", "--version"] for args, _ in self.calls))
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Optional private desktops never change normal installation/readiness."""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'packaging'))
|
||||
from build_expanded import package_relationships
|
||||
from build_profiles import PROFILES, target_profile
|
||||
from framebridge_expanded import cli, config, display_context as dc, runtime
|
||||
|
||||
|
||||
class DesktopPackageRelationships(unittest.TestCase):
|
||||
def test_no_automatic_virtual_desktop_in_any_target(self):
|
||||
for target in PROFILES:
|
||||
with self.subTest(target=target):
|
||||
fields = package_relationships(target_profile(target))
|
||||
def packages(value):
|
||||
return {re.match(r'[a-z0-9+.-]+', item.strip()).group()
|
||||
for item in re.split(r'[,|]', value) if item.strip()}
|
||||
required = packages(fields['Depends'])
|
||||
recommended = packages(fields.get('Recommends', ''))
|
||||
self.assertTrue({'openbox', 'xvfb'}.isdisjoint(required | recommended))
|
||||
self.assertEqual({'openbox', 'xvfb'}, set(fields['Suggests'].split(', ')))
|
||||
self.assertIn('xauth', required)
|
||||
self.assertIn('tmux', required)
|
||||
|
||||
|
||||
class OptionalPrivateDesktop(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(prefix='fb-optional-desktop-')
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.state = Path(self.temporary.name)
|
||||
config.save_config(self.state, config.default_config())
|
||||
self.env = {'PATH': '/fixture/bin'}
|
||||
dc.bind_text(self.env, self.state)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def prerequisites(self, missing, physical=False):
|
||||
def which(name, path=None):
|
||||
return None if name in missing else '/fixture/bin/' + name
|
||||
compatible = {'ok': True, 'profile_id': 'debian13-amd64-py313',
|
||||
'issues': [], 'warnings': []}
|
||||
context = {'kind': 'physical-x11', 'physical': True} if physical else {'kind': 'text', 'physical': False}
|
||||
with patch.object(runtime, 'environment', return_value=dict(self.env)), \
|
||||
patch.object(runtime.shutil, 'which', side_effect=which), \
|
||||
patch.object(runtime, '_display_ok', return_value=physical), \
|
||||
patch.object(dc, 'describe', return_value=context), \
|
||||
patch('framebridge_expanded.network.inspect', return_value={}), \
|
||||
patch('framebridge_expanded.compatibility.assess', return_value=compatible):
|
||||
yield
|
||||
|
||||
def mode(self, value):
|
||||
settings = config.load_config(self.state)
|
||||
settings['headless'] = value
|
||||
config.save_config(self.state, settings)
|
||||
|
||||
def test_normal_text_mode_ready_without_optional_tools(self):
|
||||
with self.prerequisites({'openbox', 'Xvfb'}):
|
||||
result = runtime.doctor(self.state)
|
||||
self.assertTrue(result['ok'])
|
||||
self.assertEqual([], result['missing_commands'])
|
||||
self.assertTrue(result['capabilities']['fbexec'])
|
||||
self.assertTrue(result['capabilities']['fbterm'])
|
||||
self.assertFalse(result['capabilities']['private_desktop'])
|
||||
self.assertEqual(['Xvfb', 'openbox'], result['optional_missing_commands']['private_desktop'])
|
||||
self.assertEqual(dc.VIRTUAL_INSTALL_COMMAND, result['optional_install_commands']['private_desktop'])
|
||||
|
||||
def test_existing_physical_x11_does_not_require_virtual_tools(self):
|
||||
self.mode('no')
|
||||
with self.prerequisites({'openbox', 'Xvfb'}, physical=True):
|
||||
result = runtime.doctor(self.state)
|
||||
self.assertTrue(result['ok'])
|
||||
self.assertTrue(result['capabilities']['graphical_cua'])
|
||||
self.assertFalse(result['capabilities']['private_desktop'])
|
||||
self.assertEqual([], result['missing_commands'])
|
||||
|
||||
def test_explicit_virtual_start_fails_before_spawning_with_install_hint(self):
|
||||
self.mode('yes')
|
||||
with self.prerequisites({'openbox'}), \
|
||||
patch.object(runtime, 'status', return_value={'running': False}), \
|
||||
patch.object(runtime, '_spawn') as spawn:
|
||||
result = runtime.start(self.state)
|
||||
self.assertFalse(result['ok'])
|
||||
self.assertIn('openbox', result['doctor']['missing_commands'])
|
||||
self.assertIn(dc.VIRTUAL_INSTALL_COMMAND, result['error'])
|
||||
spawn.assert_not_called()
|
||||
|
||||
def test_manager_rechecks_missing_tool_before_creating_any_child(self):
|
||||
self.mode('yes')
|
||||
with self.prerequisites({'openbox'}), \
|
||||
patch.object(runtime.signal, 'signal'), \
|
||||
patch.object(runtime, '_spawn') as spawn:
|
||||
with self.assertRaisesRegex(RuntimeError, 'Private virtual desktop tools are missing: openbox'):
|
||||
runtime._manager(self.state)
|
||||
spawn.assert_not_called()
|
||||
error = json.loads((self.state/'last-error.json').read_text())['error']
|
||||
self.assertIn(dc.VIRTUAL_INSTALL_COMMAND, error)
|
||||
|
||||
def test_virtual_tools_available_with_no_optional_install_hint(self):
|
||||
with self.prerequisites(set()):
|
||||
result = runtime.doctor(self.state)
|
||||
self.assertTrue(result['capabilities']['private_desktop'])
|
||||
self.assertIsNone(result['optional_install_commands']['private_desktop'])
|
||||
|
||||
def test_human_doctor_explains_optional_install_without_failing(self):
|
||||
fake_runtime = SimpleNamespace(doctor=Mock(return_value={
|
||||
'ok': True, 'display': 'text tools', 'missing_commands': [], 'issues': [],
|
||||
'platform_compatibility': {'ok': True},
|
||||
'optional_missing_commands': {'private_desktop': ['openbox']},
|
||||
'optional_install_commands': {'private_desktop': dc.VIRTUAL_INSTALL_COMMAND}}))
|
||||
stdout = io.StringIO()
|
||||
with patch.object(cli, '_module', return_value=fake_runtime), \
|
||||
contextlib.redirect_stdout(stdout):
|
||||
code = cli.main(['--state-dir', str(self.state), 'doctor'])
|
||||
self.assertEqual(0, code)
|
||||
self.assertIn('Optional private desktop tools missing: openbox', stdout.getvalue())
|
||||
self.assertIn(dc.VIRTUAL_INSTALL_COMMAND, stdout.getvalue())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -81,7 +81,10 @@ class UninstallTests(unittest.TestCase):
|
||||
def test_all_users_drops_privileges_and_includes_service_accounts(self):
|
||||
accounts = [SimpleNamespace(pw_name="testservice", pw_uid=104, pw_dir="/var/lib/testservice"), SimpleNamespace(pw_name="testperson", pw_uid=1000, pw_dir="/home/testperson"), SimpleNamespace(pw_name="nobody", pw_uid=65534, pw_dir="/nonexistent")]
|
||||
run = mock.Mock(return_value=SimpleNamespace(returncode=0))
|
||||
with mock.patch.object(uninstall.os, "getuid", return_value=0):uninstall.cleanup_all_users(run=run, accounts=accounts)
|
||||
with mock.patch.object(uninstall.os, "getuid", return_value=0), \
|
||||
mock.patch.object(uninstall, "purge_credential_helpers") as purge:
|
||||
uninstall.cleanup_all_users(run=run, accounts=accounts)
|
||||
purge.assert_called_once_with(run=run)
|
||||
self.assertEqual(run.call_count, 2)
|
||||
for call, name in zip(run.call_args_list, ["testservice", "testperson"]):
|
||||
args = call.args[0]
|
||||
|
||||
Reference in new issue
Block a user