freedombox Debian release 24.5

-----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmXdU4cWHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICMstD/9zF868IP5XxLe2+ZV8QNkHeBpX
 ElOWKA6uK7jrcR39oMX7JJnRDWFNE3IG44f3GWDfKQ8Vpann3XNGhcRvR9rhlsq6
 TEU9y518WsCLvfkFZ7TVOrx3CVXpBzrJClt3kvIGoT/UB4z2IQ4p5Vsuei1TiD1K
 R5hBBQBSTDxKIIqv+whHLaaWnUI/d2vlZeZuddfI8BPT2juhvOTxlaLlMIxXdzhU
 IMbsFMbOaFy9+F/OyWs/euLNHhGoNEwqruFL/JuACBY5UtfQpnyHozY7dq/T09Ho
 6djoLpvhVK808Vx6tNicXeC1XpARsQ6brBPasB96A7q9rYuDY++QP9Mexj7YLZJ/
 Q4TRAKsRGtIMbg411QoRO5bQqag67PLZ/b+0UwJDgwbQrzsnKI8iC4LOrxvU2rMI
 Uo1yWcnFxJZ8P7xZ+DStQU830ssddm78w38ja2R8qoNdxUS/RODz9uZxLSNoF9+v
 RKt6pf72SlJXXOiZdTgNIiuh2o2n4J/SdKxZtjeFeqHS9+5HoD4LgA28rE5ElGTQ
 fP5KGzp+X6v6TcvgIbhJ1yX2SEm5/SiuAnmPwGQ5ItjNh9SXPEU/7ekt7u5iDkf4
 x7ZyD43dmk8y6cO4v6zQFOqCyd7m0+hgFv5UwDonpOd2e6CqI7xQzMyWFofZdQbv
 PseKweEOq9y0frebYg==
 =iF1H
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmXjWscWHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICL/ZD/oC5JkU8BA6lIbhxoP4Z4Vgg5Dw
 hL9Gbh0VPyV87ZNChxvp9JUFiVrrj44WMydtv8KE+GD5Dc8byp2Ipr4cPtAcMy36
 f40DSbxPwD59ku+BlyOhQpCk5C0wgG0hageWFg27X77FTedNo7PQGpVryrZY/MTY
 aM9bL5hmlJoFvOoC6oqiF11yRdZJ24cUNSIn/JXcBs7bMhepW/6y/sDb719ZBHwG
 QtTPMYSWlrKsUIeKYkO8v3rBht2gGw851PhwmnqZ8oTEhnYc6XU/kdcyon6hFEiG
 fDSEw5X3OwJdxzR7CTXPD7qN2lZsA84GS7iyNafEDVa6hZmAV88T1xbD0+QoPz+V
 6S149azfG7QFMx/UVs5Cpf73pIKZvRH1oxHjhX55DOWk9a0B4IAwR3sRFFsfZPsd
 A0QL6A9qmdoT7+eWy9VTqyqkKFMm0hbyTVNDIemxhVkQHiUqveK1XmzdQ+RNBerL
 g14T1XBePwxy8oAFsY7uw4PP8QLv7vk4F4/cuCrpGYtqrU/+iTIZMp57kO15LIwT
 81+L+F7KaN2XvWBdEgPB6Xt3vqbR+sr6EJmhp+4BKxevkXRtSCpBkC7GHP/p7ktO
 yeoz/4HtI6q3o+1mqbllR7a4tBEM8qc+D5CzwPz6MCGCHdQqx5cMC+xgbo5EAduT
 hICdutjwRgmJ4QeDnQ==
 =1fNa
 -----END PGP SIGNATURE-----

Merge tag 'v24.5' into debian/bookworm-backports

freedombox Debian release 24.5
This commit is contained in:
James Valleroy 2024-03-02 11:58:46 -05:00
commit 14d69e2cf7
19 changed files with 305 additions and 89 deletions

View File

@ -159,6 +159,7 @@ set -xe pipefail
cd /freedombox/
sudo apt-get -y install make
sudo make provision-dev
echo 'alias freedombox-develop="cd /freedombox; sudo -u plinth /freedombox/run --develop"' \

27
debian/changelog vendored
View File

@ -1,3 +1,30 @@
freedombox (24.5) unstable; urgency=medium
[ Sunil Mohan Adapa ]
* container: Fix issue with missing make command on stable image
* setup: Minor refactoring of force upgrader class instantiation
* setup: Ensure that force upgrade won't run when app is not installed
* setup: Ensure that apt is updated before checking force upgrade
* firewalld: Implement force upgrading to any 2.x versions
* backups: tests: Don't use pytest marks on fixtures
* tor: tests: Fix issue with pytest 8.x versions
* tor: tests: Convert to pytest style tests from class based tests
* pyproject.toml: Exclude the build directory from mypy checks
* gitweb, users: Minor fixes for newer pycodestyle
* daemon: Add new component for daemons shared across apps
* wordpress: Add shared daemon component for mariadb/mysql
* zoph: Add shared daemon component for mariadb/mysql
[ James Valleroy ]
* setup: Try force upgrade before running app setup
* tests: Patch apps_init for enable/disable daemon test
* doc: Fetch latest manual
[ Olaf Schaf ]
* Translated using Weblate (German)
-- James Valleroy <jvalleroy@mailbox.org> Mon, 26 Feb 2024 20:58:45 -0500
freedombox (24.4~bpo12+1) bookworm-backports; urgency=medium
* Rebuild for bookworm-backports.

View File

@ -8,3 +8,6 @@ Daemon
.. autoclass:: plinth.daemon.RelatedDaemon
:members:
.. autoclass:: plinth.daemon.SharedDaemon
:members:

View File

@ -8,6 +8,25 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 24.5 (2024-02-26) ==
* backups: tests: Don't use pytest marks on fixtures
* container: Fix issue with missing make command on stable image
* daemon: Add new component for daemons shared across apps
* firewalld: Implement force upgrading to any 2.x versions
* gitweb, users: Minor fixes for newer pycodestyle
* locale: Update translations for German
* pyproject.toml: Exclude the build directory from mypy checks
* setup: Ensure that apt is updated before checking force upgrade
* setup: Ensure that force upgrade won't run when app is not installed
* setup: Minor refactoring of force upgrader class instantiation
* setup: Try force upgrade before running app setup
* tests: Patch apps_init for enable/disable daemon test
* tor: tests: Convert to pytest style tests from class based tests
* tor: tests: Fix issue with pytest 8.x versions
* wordpress: Add shared daemon component for mariadb/mysql
* zoph: Add shared daemon component for mariadb/mysql
== FreedomBox 24.4 (2024-02-12) ==
=== Highlights ===

View File

@ -8,6 +8,25 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 24.5 (2024-02-26) ==
* backups: tests: Don't use pytest marks on fixtures
* container: Fix issue with missing make command on stable image
* daemon: Add new component for daemons shared across apps
* firewalld: Implement force upgrading to any 2.x versions
* gitweb, users: Minor fixes for newer pycodestyle
* locale: Update translations for German
* pyproject.toml: Exclude the build directory from mypy checks
* setup: Ensure that apt is updated before checking force upgrade
* setup: Ensure that force upgrade won't run when app is not installed
* setup: Minor refactoring of force upgrader class instantiation
* setup: Try force upgrade before running app setup
* tests: Patch apps_init for enable/disable daemon test
* tor: tests: Convert to pytest style tests from class based tests
* tor: tests: Fix issue with pytest 8.x versions
* wordpress: Add shared daemon component for mariadb/mysql
* zoph: Add shared daemon component for mariadb/mysql
== FreedomBox 24.4 (2024-02-12) ==
=== Highlights ===

View File

@ -3,4 +3,4 @@
Package init file.
"""
__version__ = '24.4'
__version__ = '24.5'

View File

@ -135,6 +135,41 @@ class RelatedDaemon(app.FollowerComponent):
self.unit = unit
class SharedDaemon(Daemon):
"""Component to manage a daemon that is used by multiple apps.
Daemons such as a database server are a hard requirement for an app.
However, there may be multiple apps using that server. This component
ensures that server is enabled and running when app is enabled. It runs
diagnostics on the daemon when app is diagnosed. The primary difference
from the Daemon component is that when the app is disabled the daemon must
only be disabled if there is no other app using this daemon.
"""
# A shared daemon may be running even when an app is disabled because
# another app might be using the daemon. Hence, the enabled/disabled state
# of this component can't be used to determine the enabled/disabled state
# of the app.
is_leader = False
def set_enabled(self, enabled):
"""Do nothing. Enabled state is still determined by unit status."""
def disable(self):
"""Disable the daemon iff this is the last app using the daemon."""
other_apps_enabled = False
for other_app in app.App.list():
if other_app.app_id == self.app_id:
continue
for component in other_app.get_components_of_type(SharedDaemon):
if component.unit == self.unit and other_app.is_enabled():
other_apps_enabled = True
if not other_apps_enabled:
super().disable()
def app_is_running(app_):
"""Return whether all the daemons in the app are running."""
for component in app_.components.values():

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: FreedomBox UI\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-12 20:35-0500\n"
"PO-Revision-Date: 2024-02-11 20:14+0000\n"
"Last-Translator: Dietmar <sagen@permondes.de>\n"
"PO-Revision-Date: 2024-02-22 14:02+0000\n"
"Last-Translator: Olaf Schaf <pakuti123000@gmail.com>\n"
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
"freedombox/de/>\n"
"Language: de\n"
@ -19,7 +19,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.4-dev\n"
"X-Generator: Weblate 5.5-dev\n"
#: config.py:120
#, python-brace-format
@ -7042,10 +7042,8 @@ msgid "Read-only root filesystem"
msgstr "Schreibgeschütztes Root-Dateisystem"
#: modules/storage/__init__.py:391
#, fuzzy
#| msgid "Go to Networks"
msgid "Go to Power"
msgstr "Zur Stromversorgung"
msgstr "Springe zur Stromversorgung"
#: modules/storage/forms.py:63
msgid "Invalid directory name."

View File

@ -59,9 +59,8 @@ def fixture_create_temp_user(temp_home, password, needs_root):
subprocess.check_call(['sudo', 'userdel', username])
@pytest.mark.usefixtures('needs_sudo')
@pytest.fixture(name='has_ssh_key', scope='module', autouse=True)
def fixture_ssh_key(temp_home, temp_user, password, needs_root):
def fixture_ssh_key(temp_home, temp_user, password, needs_root, needs_sudo):
subprocess.check_call([
'sudo', '-n', '-u', temp_user, 'ssh-keygen', '-t', 'rsa', '-b', '1024',
'-N', '', '-f', f'{temp_home}/.ssh/id_rsa', '-q'

View File

@ -87,9 +87,9 @@ class FirewallApp(app_module.App):
if 'firewalld' not in packages:
return False
# Allow upgrade from any version to any version below 2.0
# Allow upgrade from any version to any version below 3.0
package = packages['firewalld']
if Version(package['new_version']) > Version('2~'):
if Version(package['new_version']) > Version('3~'):
return False
install(['firewalld'], force_configuration='new')

View File

@ -53,7 +53,7 @@ def gitweb_patch():
with patch('plinth.modules.gitweb.get_repo_list') as get_repo_list, \
patch('plinth.app.App.get') as app_get, \
patch(f'{privileged}.create_repo'), \
patch(f'{privileged}.repo_exists') as repo_exists,\
patch(f'{privileged}.repo_exists') as repo_exists, \
patch(f'{privileged}.repo_info') as repo_info, \
patch(f'{privileged}.rename_repo'), \
patch(f'{privileged}.set_repo_description'), \

View File

@ -10,7 +10,7 @@ from django.utils.translation import gettext_noop
from plinth import action_utils
from plinth import app as app_module
from plinth import cfg, kvstore, menu
from plinth import setup as setup_module
from plinth import setup as setup_module_ # Not setup_module, for pytest
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
diagnose_port_listening)
from plinth.modules import torproxy
@ -209,7 +209,7 @@ class TorApp(app_module.App):
kvstore.set(torproxy.PREINSTALL_CONFIG_KEY, json.dumps(config))
# This creates the operation, which will run after the current
# operation (Tor setup) is completed.
setup_module.run_setup_on_app('torproxy')
setup_module_.run_setup_on_app('torproxy')
if not old_version:
logger.info('Enabling Tor app')

View File

@ -11,72 +11,64 @@ from django.core.exceptions import ValidationError
from plinth.modules.tor import forms, utils
class TestTor:
"""Test cases for testing the Tor module."""
@patch('plinth.app.App.get')
@pytest.mark.usefixtures('needs_root', 'load_cfg')
def test_get_status(_app_get):
"""Test that get_status does not raise any unhandled exceptions.
@staticmethod
@patch('plinth.app.App.get')
@pytest.mark.usefixtures('needs_root', 'load_cfg')
def test_get_status(_app_get):
"""Test that get_status does not raise any unhandled exceptions.
This should work regardless of whether tor is installed, or
/etc/tor/instances/plinth/torrc exists.
"""
utils.get_status()
This should work regardless of whether tor is installed, or
/etc/tor/instances/plinth/torrc exists.
"""
utils.get_status()
class TestTorForm:
"""Test whether Tor configration form works."""
def test_bridge_validator():
"""Test upstream bridges' form field validator."""
validator = forms.bridges_validator
@staticmethod
def test_bridge_validator():
"""Test upstream bridges' form field validator."""
validator = forms.bridges_validator
# Just IP:port
validator('73.237.165.184:9001')
validator('73.237.165.184')
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]')
# Just IP:port
validator('73.237.165.184:9001')
validator('73.237.165.184')
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]')
# With fingerprint
validator('73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
# With fingerprint
validator('73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
# With transport type
validator('obfs4 73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
# With transport type
validator('obfs4 73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
# With transport type and extra options
validator('obfs4 10.1.1.1:30000 '
'0123456789ABCDEF0123456789ABCDEF01234567 '
'cert=A/b+1 iat-mode=0')
# With transport type and extra options
validator('obfs4 10.1.1.1:30000 '
'0123456789ABCDEF0123456789ABCDEF01234567 '
'cert=A/b+1 iat-mode=0')
# Leading, trailing spaces and empty lines
validator('\n'
' \n'
'73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
' \n'
'73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
' \n'
'\n')
# Leading, trailing spaces and empty lines
validator('\n'
' \n'
'73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
' \n'
'73.237.165.184:9001 '
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
' \n'
'\n')
# Invalid number for parts
with pytest.raises(ValidationError):
validator(' ')
# Invalid number for parts
with pytest.raises(ValidationError):
validator(' ')
# Invalid IP address/port
with pytest.raises(ValidationError):
validator('73.237.165.384:9001')
# Invalid IP address/port
with pytest.raises(ValidationError):
validator('73.237.165.384:9001')
with pytest.raises(ValidationError):
validator('73.237.165.184:90001')
with pytest.raises(ValidationError):
validator('73.237.165.184:90001')
with pytest.raises(ValidationError):
validator('[a2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
with pytest.raises(ValidationError):
validator('[a2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
with pytest.raises(ValidationError):
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:90443')
with pytest.raises(ValidationError):
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:90443')

View File

@ -72,9 +72,9 @@ def make_request(request, view, as_admin=True, **kwargs):
request.user = admin_user if as_admin else user
with patch('plinth.modules.users.forms.is_user_admin',
return_value=as_admin),\
return_value=as_admin), \
patch('plinth.modules.users.views.is_user_admin',
return_value=as_admin),\
return_value=as_admin), \
patch('plinth.modules.users.views.update_session_auth_hash'):
response = view(request, **kwargs)

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.daemon import Daemon, SharedDaemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
@ -106,6 +106,9 @@ class WordPressApp(app_module.App):
daemon = Daemon('daemon-wordpress', 'wordpress-freedombox.timer')
self.add(daemon)
daemon = SharedDaemon('shared-daemon-wordpress-mysql', 'mysql')
self.add(daemon)
backup_restore = WordPressBackupRestore('backup-restore-wordpress',
**manifest.backup)
self.add(backup_restore)

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import SharedDaemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
@ -89,6 +90,9 @@ class ZophApp(app_module.App):
webserver = Webserver('webserver-zoph-freedombox', 'zoph-freedombox')
self.add(webserver)
daemon = SharedDaemon('shared-daemon-zoph-mysql', 'mysql')
self.add(daemon)
backup_restore = ZophBackupRestore('backup-restore-zoph',
**manifest.backup)
self.add(backup_restore)

View File

@ -26,8 +26,6 @@ _is_first_setup = False
is_first_setup_running = False
_is_shutting_down = False
_force_upgrader = None
def run_setup_on_app(app_id, allow_install=True, rerun=False):
"""Execute the setup process in a thread."""
@ -59,6 +57,15 @@ def _run_setup_on_app(app, current_version):
message = None
try:
current_version = app.get_setup_version()
if current_version != 0:
# Check if this app needs force_upgrade. If it is needed, but not
# yet supported for the new version of the package, then an
# exception will be raised, so that we do not run setup.
package.refresh_package_lists()
force_upgrader = ForceUpgrader.get_instance()
force_upgrader.attempt_upgrade_for_app(app.app_id)
app.setup(old_version=current_version)
app.set_setup_version(app.info.version)
post_setup.send_robust(sender=app.__class__, module_name=app.app_id)
@ -143,8 +150,8 @@ def stop():
global _is_shutting_down
_is_shutting_down = True
if _force_upgrader:
_force_upgrader.shutdown()
force_upgrader = ForceUpgrader.get_instance()
force_upgrader.shutdown()
def setup_apps(app_ids=None, essential=False, allow_install=True):
@ -229,8 +236,8 @@ def _get_apps_for_regular_setup():
1. essential apps that are not up-to-date
2. non-essential app that are installed and need updates
"""
if (app.info.is_essential and
app.get_setup_state() != app_module.App.SetupState.UP_TO_DATE):
if (app.info.is_essential and app.get_setup_state()
!= app_module.App.SetupState.UP_TO_DATE):
return True
if app.get_setup_state() == app_module.App.SetupState.NEEDS_UPDATE:
@ -335,6 +342,7 @@ class ForceUpgrader():
"""
_instance = None
_run_lock = threading.Lock()
_wait_event = threading.Event()
@ -349,6 +357,14 @@ class ForceUpgrader():
"""Raised when upgrade fails and there is nothing more we wish to do.
"""
@classmethod
def get_instance(cls):
"""Return a single instance of a the class."""
if not cls._instance:
cls._instance = ForceUpgrader()
return cls._instance
def __init__(self):
"""Initialize the force upgrader."""
if plinth.cfg.develop:
@ -401,7 +417,7 @@ class ForceUpgrader():
def shutdown(self):
"""If we are sleeping for next attempt, cancel it.
If we are actually upgrading packages, don nothing.
If we are actually upgrading packages, do nothing.
"""
self._wait_event.set()
@ -448,6 +464,50 @@ class ForceUpgrader():
if need_retry:
raise self.TemporaryFailure('Some apps failed to force upgrade.')
def attempt_upgrade_for_app(self, app_id):
"""Attempt to perform an upgrade for specified app.
Raise TemporaryFailure if upgrade can't be performed now.
Raise PermanentFailure if upgrade can't be performed until something
with the system state changes. We don't want to try again until
notified of further package cache changes.
Return True if upgrade was performed successfully.
Return False if upgrade is not needed.
"""
if _is_shutting_down:
raise self.PermanentFailure('Service is shutting down')
if packages_privileged.is_package_manager_busy():
raise self.TemporaryFailure('Package manager is busy')
apps = self._get_list_of_apps_to_force_upgrade()
if app_id not in apps:
logger.info('App %s does not need force upgrade', app_id)
return False
packages = apps[app_id]
app = app_module.App.get(app_id)
try:
logger.info('Force upgrading app: %s', app.info.name)
if app.force_upgrade(packages):
logger.info('Successfully force upgraded app: %s',
app.info.name)
return True
else:
logger.warning('Ignored force upgrade for app: %s',
app.info.name)
raise self.TemporaryFailure(
'Force upgrade is needed, but not yet implemented for new '
f'version of app: {app_id}')
except Exception as exception:
logger.exception('Error running force upgrade: %s', exception)
raise self.TemporaryFailure(
f'App {app_id} failed to force upgrade.')
def _run_force_upgrade_as_operation(self, app, packages):
"""Start an operation for force upgrading."""
name = gettext_noop('Updating app packages')
@ -523,8 +583,5 @@ class ForceUpgrader():
def on_package_cache_updated():
"""Called by D-Bus service when apt package cache is updated."""
global _force_upgrader
if not _force_upgrader:
_force_upgrader = ForceUpgrader()
_force_upgrader.on_package_cache_updated()
force_upgrader = ForceUpgrader.get_instance()
force_upgrader.on_package_cache_updated()

View File

@ -10,7 +10,7 @@ from unittest.mock import Mock, call, patch
import pytest
from plinth.app import App, FollowerComponent, Info
from plinth.daemon import (Daemon, RelatedDaemon, app_is_running,
from plinth.daemon import (Daemon, RelatedDaemon, SharedDaemon, app_is_running,
diagnose_netcat, diagnose_port_listening)
from plinth.modules.diagnostics.check import DiagnosticCheck, Result
@ -77,10 +77,11 @@ def test_is_enabled(service_is_enabled, daemon):
service_is_enabled.assert_has_calls([call('test-unit', strict_check=True)])
@patch('plinth.app.apps_init')
@patch('subprocess.run')
@patch('subprocess.call')
def test_enable(subprocess_call, subprocess_run, app_list, mock_privileged,
daemon):
def test_enable(subprocess_call, subprocess_run, apps_init, app_list,
mock_privileged, daemon):
"""Test that enabling the daemon works."""
daemon.enable()
subprocess_call.assert_has_calls(
@ -101,9 +102,11 @@ def test_enable(subprocess_call, subprocess_run, app_list, mock_privileged,
stdout=subprocess.DEVNULL, check=False)
@patch('plinth.app.apps_init')
@patch('subprocess.run')
@patch('subprocess.call')
def test_disable(subprocess_call, subprocess_run, mock_privileged, daemon):
def test_disable(subprocess_call, subprocess_run, apps_init, app_list,
mock_privileged, daemon):
"""Test that disabling the daemon works."""
daemon.disable()
subprocess_call.assert_has_calls(
@ -329,3 +332,56 @@ def test_related_daemon_initialization():
with pytest.raises(ValueError):
RelatedDaemon(None, 'test-daemon')
def test_shared_daemon_leader():
"""Test that shared daemon is not a leader component."""
component1 = SharedDaemon('test-component1', 'test-daemon')
assert not component1.is_leader
@patch('plinth.action_utils.service_is_enabled')
def test_shared_daemon_set_enabled(service_is_enabled):
"""Test that enabled status is determined by unit status."""
component = SharedDaemon('test-component', 'test-daemon')
service_is_enabled.return_value = False
component.set_enabled(False)
assert not component.is_enabled()
component.set_enabled(True)
assert not component.is_enabled()
service_is_enabled.return_value = True
component.set_enabled(False)
assert component.is_enabled()
component.set_enabled(True)
assert component.is_enabled()
@patch('plinth.privileged.service.disable')
def test_shared_daemon_disable(disable_method):
"""Test that shared daemon disables service correctly."""
class AppTest2(App):
"""Test application class."""
app_id = 'test-app-2'
component1 = SharedDaemon('test-component1', 'test-daemon')
app1 = AppTest()
app1.add(component1)
app1.is_enabled = Mock()
component2 = SharedDaemon('test-component2', 'test-daemon')
app2 = AppTest2()
app2.add(component2)
# When another app is enabled, service should not be disabled
app1.is_enabled.return_value = True
app2.disable()
assert disable_method.mock_calls == []
# When all other apps are disabled, service should be disabled
disable_method.reset_mock()
app1.is_enabled.return_value = False
app2.disable()
assert disable_method.mock_calls == [call('test-daemon')]

View File

@ -201,6 +201,9 @@ disable = [
"too-many-ancestors", # Easy to hit when using Django
]
[tool.mypy]
exclude = "build/"
# Ignore missing type stubs for some libraries. Try to keep this list minimal
# and use type annotations where available.
[[tool.mypy.overrides]]