mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
Merge remote-tracking branch 'freedombox-team/master'
Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
commit
dfaeca4580
275
plinth/modules/help/tests/test_views.py
Normal file
275
plinth/modules/help/tests/test_views.py
Normal file
@ -0,0 +1,275 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Tests for help views.
|
||||
|
||||
Design: - Make tests independent from URL policy by using Django names instead
|
||||
of URLs to call the help module. For this, some additional fixture
|
||||
work is needed: pytestmark and fixture_app_urls().
|
||||
|
||||
Pending: - status log
|
||||
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django import urls
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
|
||||
from plinth import module_loader
|
||||
from plinth.modules.help import views
|
||||
|
||||
# For all tests, use plinth.urls instead of urls configured for testing
|
||||
pytestmark = pytest.mark.urls('plinth.urls')
|
||||
|
||||
|
||||
def _is_page(response):
|
||||
"""Minimal check on help views."""
|
||||
return (response.status_code == 200 and 'title' in response.context_data
|
||||
and response['content-type'] == 'text/html; charset=utf-8')
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='module')
|
||||
def fixture_app_urls():
|
||||
"""Make sure app's URLs are part of plinth.urls."""
|
||||
with patch('plinth.module_loader._modules_to_load', new=[]) as modules, \
|
||||
patch('plinth.urls.urlpatterns', new=[]):
|
||||
modules.append('plinth.modules.help')
|
||||
module_loader.include_urls()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize("view_name, view", (
|
||||
('contribute', views.contribute),
|
||||
('feedback', views.feedback),
|
||||
('support', views.support),
|
||||
('index', views.index),
|
||||
))
|
||||
def test_simple_help_pages(rf, view_name, view):
|
||||
"""Simple common test for certain help views."""
|
||||
response = view(rf.get(urls.reverse('help:' + view_name)))
|
||||
assert _is_page(response)
|
||||
|
||||
|
||||
def test_about(rf):
|
||||
"""Test some expected items in about view."""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
response = views.about(rf.get(manual_url))
|
||||
assert _is_page(response)
|
||||
for item in ('version', 'new_version', 'os_release'):
|
||||
assert item in response.context_data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for serving the offline user guide ( the "manual")
|
||||
#
|
||||
# The manual can be requested:
|
||||
# - Either complete on a single page or page by page.
|
||||
# - Specifying (or not) the language.
|
||||
# - The complete manual can be requested in HTML or PDF formats.
|
||||
#
|
||||
# Expected Behaviour Rules:
|
||||
# - If the page isn't specified, the help module returns the full manual in
|
||||
# one single page.
|
||||
# - The help module tries first to return the page in the specified
|
||||
# language. If not found (either that page doesn't exist or the language
|
||||
# wasn't secified) it falls back to its twin in the fallback language. If
|
||||
# it is neither available, it shows a proper error message.
|
||||
#
|
||||
# Design Decisions:
|
||||
# - The PDF manual has a separate function to serve it.
|
||||
# - The 'Manual' page doesn't exist as such. However there are files named
|
||||
# 'Manual' containing the complete full manual in one single page.
|
||||
# - In order to avoid loops, the fallback language is intercepted and
|
||||
# treated specifically.
|
||||
# - Problem: Requesting a missing page in a language that happens to be
|
||||
# the fallback one, missing it would cause the help module to
|
||||
# redirect to the same page, closing thereby a neverending loop.
|
||||
# The web served would probably break that loop, but it would
|
||||
# cause confusion to the user.
|
||||
# - CI environments don't setup FreedomBox completely. A regular setup run is
|
||||
# impractically slow (10-15 mins), if even posible. Compiling and deploying
|
||||
# the manual is just 3 extra lines in .gitlab-ci.yml file:
|
||||
# - make -C doc
|
||||
# - mkdir -p /usr/share/freedombox/manual
|
||||
# - cp -r doc/manual /usr/share/freedombox/manual
|
||||
# But again, this causes the 4 minutes of test preparation to bump to 6.
|
||||
# It's not worth for just testing the offline manual, so the tests guess if
|
||||
# they are running in a restricted environment and skip.
|
||||
|
||||
canary = pathlib.Path('doc/manual/en/Coturn.part.html')
|
||||
TRANSLATIONS = ('es', )
|
||||
MANUAL_PAGES = ('Apache_userdir', 'APU', 'Backups', 'BananaPro', 'BeagleBone',
|
||||
'bepasty', 'Bind', 'Calibre', 'Cockpit', 'Configure',
|
||||
'Contribute', 'Coturn', 'Cubieboard2', 'Cubietruck',
|
||||
'DateTime', 'Debian', 'Deluge', 'Developer', 'Diagnostics',
|
||||
'Download', 'DynamicDNS', 'ejabberd', 'Firewall',
|
||||
'freedombox-manual', 'GettingHelp', 'GitWeb', 'Hardware',
|
||||
'I2P', 'Ikiwiki', 'Infinoted', 'Introduction', 'JSXC',
|
||||
'LetsEncrypt', 'Maker', 'MatrixSynapse', 'MediaWiki',
|
||||
'Minetest', 'MiniDLNA', 'MLDonkey', 'Monkeysphere', 'Mumble',
|
||||
'NameServices', 'Networks', 'OpenVPN', 'OrangePiZero',
|
||||
'PageKite', 'pcDuino3', 'Performance', 'PineA64+',
|
||||
'PioneerEdition', 'Plinth', 'Power', 'Privoxy', 'Quassel',
|
||||
'QuickStart', 'Radicale', 'RaspberryPi2', 'RaspberryPi3B+',
|
||||
'RaspberryPi3B', 'RaspberryPi4B', 'ReleaseNotes', 'Rock64',
|
||||
'RockPro64', 'Roundcube', 'Samba', 'Searx', 'SecureShell',
|
||||
'Security', 'ServiceDiscovery', 'Shadowsocks', 'Sharing',
|
||||
'Snapshots', 'Storage', 'Syncthing', 'TinyTinyRSS', 'Tor',
|
||||
'Transmission', 'Upgrades', 'USBWiFi', 'Users', 'VirtualBox',
|
||||
'WireGuard')
|
||||
_restricted_reason = ('Needs installed manual. '
|
||||
'CI speed-optimized workspace does not provide it.')
|
||||
not_restricted_environment = pytest.mark.skipif(not canary.exists(),
|
||||
reason=_restricted_reason)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('lang', (None, '-'))
|
||||
def test_full_default_manual(rf, lang):
|
||||
"""Test request for the full default manual.
|
||||
|
||||
Expected: Redirect to the full manual in the fallback language.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
response = views.manual(rf.get(manual_url), lang=lang)
|
||||
assert response.status_code == 302
|
||||
assert response.url == '/help/manual/en/'
|
||||
|
||||
# With a language cookie set
|
||||
request = rf.get(manual_url)
|
||||
request.COOKIES[settings.LANGUAGE_COOKIE_NAME] = TRANSLATIONS[0]
|
||||
response = views.manual(request, lang=lang)
|
||||
assert response.status_code == 302
|
||||
assert response.url == f'/help/manual/{TRANSLATIONS[0]}/'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('lang', (None, '-'))
|
||||
def test_default_manual_by_pages(rf, lang):
|
||||
"""Test page-specific requests for the (default) manual.
|
||||
|
||||
Expected: Redirect to their respective twins in the fallback language.
|
||||
Pending.: Redirect pages with plus-sign '+' in their name.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
for page in MANUAL_PAGES:
|
||||
if '+' in page or 'Manual' in page: # Pine64+ & RaspberryPi3B+
|
||||
continue
|
||||
|
||||
response = views.manual(rf.get(manual_url), lang=lang, page=page)
|
||||
assert response.status_code == 302
|
||||
assert response.url == '/help/manual/en/' + page
|
||||
|
||||
# With a language cookie set
|
||||
request = rf.get(manual_url)
|
||||
request.COOKIES[settings.LANGUAGE_COOKIE_NAME] = TRANSLATIONS[0]
|
||||
response = views.manual(request, lang=lang, page=page)
|
||||
assert response.status_code == 302
|
||||
assert response.url == f'/help/manual/{TRANSLATIONS[0]}/{page}'
|
||||
|
||||
|
||||
@not_restricted_environment
|
||||
def test_specific_full_manual_translation(rf):
|
||||
"""Test request for specific translated manuals.
|
||||
|
||||
Expected: All return a page.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
for lang in ('es', 'en'):
|
||||
response = views.manual(rf.get(manual_url), lang=lang)
|
||||
assert _is_page(response)
|
||||
|
||||
|
||||
@not_restricted_environment
|
||||
def test_specific_manual_translation_by_pages(rf):
|
||||
"""Test that translated-page-specific requests.
|
||||
|
||||
Expected: All known page names return pages.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
for lang in ('es', 'en'):
|
||||
for page in MANUAL_PAGES:
|
||||
response = views.manual(rf.get(manual_url), page=page, lang=lang)
|
||||
assert _is_page(response)
|
||||
|
||||
|
||||
@not_restricted_environment
|
||||
def test_full_manual_requested_by_page_name(rf):
|
||||
"""Test requests for 'Manual'.
|
||||
|
||||
Note: 'Manual' is a file, not a manual page.
|
||||
Expected: Return a proper not found message (HTTP 404)
|
||||
Currently: Non fallback languages return a page.
|
||||
This is wrong, but doesn't cause any harm.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
page = 'Manual'
|
||||
|
||||
for lang in TRANSLATIONS:
|
||||
response = views.manual(rf.get(manual_url), page=page, lang=lang)
|
||||
assert _is_page(response)
|
||||
|
||||
with pytest.raises(Http404):
|
||||
views.manual(rf.get(manual_url), page=page, lang='en')
|
||||
|
||||
|
||||
def test_missing_page(rf):
|
||||
"""Test requests for missing pages.
|
||||
|
||||
Expected:
|
||||
- Unspecified language: Fall back to its fallback twin.
|
||||
- Translated languages: Fall back to its fallback twin.
|
||||
- Fallback language...: Return a proper not found message (HTTP 404)
|
||||
- Unknown languages...: Fall back to its fallback twin.
|
||||
|
||||
"""
|
||||
manual_url = urls.reverse('help:manual')
|
||||
page = 'unknown'
|
||||
for lang in TRANSLATIONS + ('unknown', None):
|
||||
response = views.manual(rf.get(manual_url), page=page, lang=lang)
|
||||
assert response.status_code == 302
|
||||
assert response.url == '/help/manual/en/unknown'
|
||||
|
||||
with pytest.raises(Http404):
|
||||
views.manual(rf.get(manual_url), page=page, lang='en')
|
||||
|
||||
|
||||
@not_restricted_environment
|
||||
def test_download_full_manual_file(rf, tmp_path):
|
||||
"""Test download of manual.
|
||||
|
||||
Design: - Downloads the default manual, a translated one and the
|
||||
fallback translation. None should fail. Then compares
|
||||
them.
|
||||
- Call diff command for fast comparision. Comparing the
|
||||
over 10MB bytestrings in python is insanely slow.
|
||||
|
||||
"""
|
||||
|
||||
def _diff(file_name_a, file_name_b, same):
|
||||
file_a = tmp_path / file_name_a
|
||||
file_b = tmp_path / file_name_b
|
||||
process = subprocess.run(
|
||||
['diff', '-q', str(file_a), str(file_b)], check=False)
|
||||
assert bool(process.returncode) != same
|
||||
|
||||
url = urls.reverse('help:manual')
|
||||
manuals = {
|
||||
'unspecified': rf.get(url),
|
||||
'translated': rf.get(url, HTTP_ACCEPT_LANGUAGE='es'),
|
||||
'fallback': rf.get(url, HTTP_ACCEPT_LANGUAGE='en')
|
||||
}
|
||||
for name, request in manuals.items():
|
||||
response = views.download_manual(request)
|
||||
assert response.status_code == 200
|
||||
file = tmp_path / (name + '.pdf')
|
||||
file.write_bytes(response.content)
|
||||
|
||||
_diff('fallback.pdf', 'unspecified.pdf', same=True)
|
||||
_diff('fallback.pdf', 'translated.pdf', same=False)
|
||||
139
plinth/tests/test_action_utils.py
Normal file
139
plinth/tests/test_action_utils.py
Normal file
@ -0,0 +1,139 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Test module for key/value store.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.action_utils import (get_addresses, get_hostname,
|
||||
is_systemd_running, service_action,
|
||||
service_disable, service_enable,
|
||||
service_is_enabled, service_is_running,
|
||||
service_reload, service_restart,
|
||||
service_start, service_stop,
|
||||
service_try_restart, service_unmask)
|
||||
|
||||
UNKNOWN = 'unknowndeamon'
|
||||
|
||||
systemctl_path = pathlib.Path('/usr/bin/systemctl')
|
||||
systemd_installed = pytest.mark.skipif(not systemctl_path.exists(),
|
||||
reason='systemd not available')
|
||||
|
||||
ip_path = pathlib.Path('/usr/bin/ip')
|
||||
ip_installed = pytest.mark.skipif(not ip_path.exists(),
|
||||
reason='ip command not available')
|
||||
|
||||
|
||||
@patch('os.path.exists')
|
||||
def test_is_systemd_running(mock):
|
||||
"""Trivial white box test for a trivial implementation."""
|
||||
mock.return_value = True
|
||||
assert is_systemd_running()
|
||||
mock.return_value = False
|
||||
assert not is_systemd_running()
|
||||
|
||||
|
||||
@systemd_installed
|
||||
def test_service_checks():
|
||||
"""Test basic checks on status of an arbitrary service."""
|
||||
assert not service_is_running(UNKNOWN)
|
||||
assert not service_is_enabled(UNKNOWN)
|
||||
|
||||
# expected is best if: generic. Alternatives: systemd-sysctl, logrotate
|
||||
expected = 'networking'
|
||||
if not service_is_running(expected):
|
||||
pytest.skip(f'Needs service {expected} running.')
|
||||
|
||||
assert service_is_enabled(expected)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('needs_root')
|
||||
@systemd_installed
|
||||
def test_service_enable_and_disable():
|
||||
"""Test enabling and disabling of an arbitrary service."""
|
||||
# service is best if: non-essential part of FreedomBox that restarts fast
|
||||
service = 'unattended-upgrades'
|
||||
if not service_is_enabled(service):
|
||||
reason = f'Needs service {service} enabled.'
|
||||
pytest.skip(reason)
|
||||
|
||||
service_disable(service)
|
||||
assert not service_is_running(service)
|
||||
service_enable(service)
|
||||
assert service_is_running(service)
|
||||
|
||||
# Ignore unknown services, don't fail:
|
||||
service_disable(UNKNOWN)
|
||||
service_enable(UNKNOWN)
|
||||
|
||||
|
||||
@patch('plinth.action_utils.service_action')
|
||||
@systemd_installed
|
||||
def test_service_actions(mock):
|
||||
"""Trivial white box test for trivial implementations."""
|
||||
service_start(UNKNOWN)
|
||||
mock.assert_called_with(UNKNOWN, 'start')
|
||||
service_stop(UNKNOWN)
|
||||
mock.assert_called_with(UNKNOWN, 'stop')
|
||||
service_restart(UNKNOWN)
|
||||
mock.assert_called_with(UNKNOWN, 'restart')
|
||||
service_try_restart(UNKNOWN)
|
||||
mock.assert_called_with(UNKNOWN, 'try-restart')
|
||||
service_reload(UNKNOWN)
|
||||
mock.assert_called_with(UNKNOWN, 'reload')
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('needs_root')
|
||||
@systemd_installed
|
||||
def test_service_unmask():
|
||||
"""Test unmasking of an arbitrary masked service."""
|
||||
|
||||
def is_masked(service):
|
||||
process = subprocess.run([
|
||||
'systemctl', 'list-unit-files', '--output=json',
|
||||
service + '.service'
|
||||
], stdout=subprocess.PIPE, check=False)
|
||||
output = json.loads(process.stdout)
|
||||
return output[0]['state'] == 'masked' if output else False
|
||||
|
||||
# SERVICE is best if: part of FreedomBox, so we can mess with least risk.
|
||||
service = 'samba-ad-dc'
|
||||
if not is_masked(service):
|
||||
pytest.skip(f'Needs service {service} masked.')
|
||||
|
||||
service_unmask(service)
|
||||
assert not is_masked(service)
|
||||
|
||||
service_action(service, 'mask')
|
||||
assert is_masked(service)
|
||||
|
||||
|
||||
def test_get_hostname():
|
||||
"""get_hostname returns a string.
|
||||
|
||||
In fact, the maximum length for a hostname is 253 characters, but
|
||||
anything longer than 80 is very suspicious, so we fail the test.
|
||||
|
||||
To avoid error messages pass as hostnames we seek and fail if we find
|
||||
some unexpected characters.
|
||||
"""
|
||||
hostname = get_hostname()
|
||||
assert hostname
|
||||
assert isinstance(hostname, str)
|
||||
assert len(hostname) < 80
|
||||
for char in ' ,:;!?=$%&@*+()[]{}<>"\'':
|
||||
assert char not in hostname
|
||||
|
||||
|
||||
@ip_installed
|
||||
def test_get_addresses():
|
||||
"""Test that any FreedomBox has some addresses."""
|
||||
ips = get_addresses()
|
||||
assert len(ips) > 3 # min: ip, 2x'localhost', hostname
|
||||
for address in ips:
|
||||
assert address['kind'] in ('4', '6')
|
||||
Loading…
x
Reference in New Issue
Block a user