tests: functional: Re-organize step definitions and helper methods

- Move non-reusable app specific step definitions and helper methods into
<app>/tests/test_functional.py.

- Merge reusable helper methods into plinth.tests.functional

- Merge reusable step definitions into plinth.tests.functional.step_definitions

- avahi, datetime, ikiwiki: Reuse common methods to avoid repetition. Avoid
mapping from app nicknames to actual app names.

- deluge, transmission: Make a copy of sample.torrent for each app to avoid
clogging common place.

- Implement functional.visit() to simplify a lot of browser.visit() calls.

- Ensure that name of the mark on functional tests for an app is same as name of
the app. This will help with predicting the mark when running tests for a
particular app.

Tests performed:

- Run all functional tests.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
Sunil Mohan Adapa 2020-05-19 23:54:55 -07:00 committed by Joseph Nuthalapati
parent 8fac6a71fe
commit 80dff7bf9c
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
53 changed files with 3340 additions and 3410 deletions

View File

@ -15,11 +15,7 @@ try:
except ImportError:
_bdd_available = False
else:
from plinth.tests.functional.step_definitions.application import *
from plinth.tests.functional.step_definitions.interface import *
from plinth.tests.functional.step_definitions.service import *
from plinth.tests.functional.step_definitions.site import *
from plinth.tests.functional.step_definitions.system import *
from plinth.tests.functional.step_definitions import *
def pytest_ignore_collect(path, config):

View File

@ -1,18 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
@system @essential @service_discovery
Feature: Service Discovery
@system @essential @avahi
Feature: Avahi Service Discovery
Configure service discovery.
Background:
Given I'm a logged in user
Scenario: Disable service discovery application
Given the service discovery application is enabled
When I disable the service discovery application
Then the service discovery service should not be running
Scenario: Disable avahi application
Given the avahi application is enabled
When I disable the avahi application
Then the avahi service should not be running
Scenario: Enable service discovery application
Given the service discovery application is disabled
When I enable the service discovery application
Then the service discovery service should be running
Scenario: Enable avahi application
Given the avahi application is disabled
When I enable the avahi application
Then the avahi service should be running

View File

@ -3,6 +3,88 @@
Functional, browser based tests for backups app.
"""
from pytest_bdd import scenarios
import os
import tempfile
import urllib.parse
import requests
from pytest import fixture
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('backups.feature')
@fixture(scope='session')
def downloaded_file_info():
return dict()
@when(parsers.parse('I open the main page'))
def open_main_page(session_browser):
_open_main_page(session_browser)
@then(parsers.parse('the main page should be shown'))
def main_page_is_shown(session_browser):
assert (session_browser.url.endswith('/plinth/'))
@when(
parsers.parse('I download the app data backup with name {archive_name:w}'))
def backup_download(session_browser, downloaded_file_info, archive_name):
file_path = _download(session_browser, archive_name)
downloaded_file_info['path'] = file_path
@when(parsers.parse('I restore the downloaded app data backup'))
def backup_restore_from_upload(session_browser, app_name,
downloaded_file_info):
path = downloaded_file_info["path"]
try:
_upload_and_restore(session_browser, app_name, path)
except Exception as err:
raise err
finally:
os.remove(path)
def _open_main_page(browser):
with functional.wait_for_page_update(browser):
browser.find_link_by_href('/plinth/').first.click()
def _download_file_logged_in(browser, url, suffix=''):
"""Download a file from Plinth, pretend being logged in via cookies"""
if not url.startswith("http"):
current_url = urllib.parse.urlparse(browser.url)
url = "%s://%s%s" % (current_url.scheme, current_url.netloc, url)
cookies = browser.driver.get_cookies()
cookies = {cookie["name"]: cookie["value"] for cookie in cookies}
response = requests.get(url, verify=False, cookies=cookies)
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
for chunk in response.iter_content(chunk_size=128):
temp_file.write(chunk)
return temp_file.name
def _download(browser, archive_name=None):
functional.nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/download/{archive_name}/'
url = functional.base_url + href
file_path = _download_file_logged_in(browser, url, suffix='.tar.gz')
return file_path
def _upload_and_restore(browser, app_name, downloaded_file_path):
functional.nav_to_module(browser, 'backups')
browser.find_link_by_href('/plinth/sys/backups/upload/').first.click()
fileinput = browser.driver.find_element_by_id('id_backups-file')
fileinput.send_keys(downloaded_file_path)
# submit upload form
functional.submit(browser)
# submit restore form
with functional.wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
functional.submit(browser)

View File

@ -3,6 +3,42 @@
Functional, browser based tests for bind app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('bind.feature')
@given(parsers.parse('bind DNSSEC is {enable:w}'))
def bind_given_enable_dnssec(session_browser, enable):
should_enable = (enable == 'enabled')
_enable_dnssec(session_browser, should_enable)
@when(parsers.parse('I {enable:w} bind DNSSEC'))
def bind_enable_dnssec(session_browser, enable):
should_enable = (enable == 'enable')
_enable_dnssec(session_browser, should_enable)
@then(parsers.parse('bind DNSSEC should be {enabled:w}'))
def bind_assert_dnssec(session_browser, enabled):
assert _get_dnssec(session_browser) == (enabled == 'enabled')
def _enable_dnssec(browser, enable):
"""Enable/disable DNSSEC in bind configuration."""
functional.nav_to_module(browser, 'bind')
if enable:
browser.check('enable_dnssec')
else:
browser.uncheck('enable_dnssec')
functional.submit(browser, form_class='form-configuration')
def _get_dnssec(browser):
"""Return whether DNSSEC is enabled/disabled in bind configuration."""
functional.nav_to_module(browser, 'bind')
return browser.find_by_name('enable_dnssec').first.checked

View File

@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
@system @essential @configuration
@system @essential @config
Feature: Configuration
Configure the system.
@ -21,4 +21,3 @@ Scenario: Change webserver home page
And the home page is syncthing
When I change the home page to plinth
Then the home page should be plinth

View File

@ -3,6 +3,75 @@
Functional, browser based tests for config app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('config.feature')
@given(parsers.parse('the home page is {app_name:w}'))
def set_home_page(session_browser, app_name):
_set_home_page(session_browser, app_name)
@when(parsers.parse('I change the hostname to {hostname:w}'))
def change_hostname_to(session_browser, hostname):
_set_hostname(session_browser, hostname)
@when(parsers.parse('I change the domain name to {domain:S}'))
def change_domain_name_to(session_browser, domain):
functional.set_domain_name(session_browser, domain)
@when(parsers.parse('I change the home page to {app_name:w}'))
def change_home_page_to(session_browser, app_name):
_set_home_page(session_browser, app_name)
@then(parsers.parse('the hostname should be {hostname:w}'))
def hostname_should_be(session_browser, hostname):
assert _get_hostname(session_browser) == hostname
@then(parsers.parse('the domain name should be {domain:S}'))
def domain_name_should_be(session_browser, domain):
assert _get_domain_name(session_browser) == domain
@then(parsers.parse('the home page should be {app_name:w}'))
def home_page_should_be(session_browser, app_name):
assert _check_home_page_redirect(session_browser, app_name)
def _get_hostname(browser):
functional.nav_to_module(browser, 'config')
return browser.find_by_id('id_hostname').value
def _set_hostname(browser, hostname):
functional.nav_to_module(browser, 'config')
browser.find_by_id('id_hostname').fill(hostname)
functional.submit(browser)
def _get_domain_name(browser):
functional.nav_to_module(browser, 'config')
return browser.find_by_id('id_domainname').value
def _set_home_page(browser, home_page):
if 'plinth' not in home_page and 'apache' not in home_page:
home_page = 'shortcut-' + home_page
functional.nav_to_module(browser, 'config')
drop_down = browser.find_by_id('id_homepage')
drop_down.select(home_page)
functional.submit(browser)
def _check_home_page_redirect(browser, app_name):
functional.visit(browser, '/')
return browser.find_by_xpath(
"//a[contains(@href, '/plinth/') and @title='FreedomBox']")

View File

@ -3,6 +3,124 @@
Functional, browser based tests for coquelicot app.
"""
from pytest_bdd import scenarios
import random
import tempfile
from pytest_bdd import given, parsers, scenarios, then, when
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from plinth.tests import functional
scenarios('coquelicot.feature')
@given('a sample local file')
def sample_local_file():
file_path, contents = _create_sample_local_file()
return dict(file_path=file_path, contents=contents)
@when(parsers.parse('I modify the maximum file size of coquelicot to {size:d}')
)
def modify_max_file_size(session_browser, size):
_modify_max_file_size(session_browser, size)
@then(parsers.parse('the maximum file size of coquelicot should be {size:d}'))
def assert_max_file_size(session_browser, size):
assert _get_max_file_size(session_browser) == size
@when(parsers.parse('I modify the coquelicot upload password to {password:w}'))
def modify_upload_password(session_browser, password):
_modify_upload_password(session_browser, password)
@then(
parsers.parse(
'I should be able to login to coquelicot with password {password:w}'))
def verify_upload_password(session_browser, password):
_verify_upload_password(session_browser, password)
@when(
parsers.parse('I upload the sample local file to coquelicot with password '
'{password:w}'))
def coquelicot_upload_file(session_browser, sample_local_file, password):
url = _upload_file(session_browser, sample_local_file['file_path'],
password)
sample_local_file['upload_url'] = url
@when('I download the uploaded file from coquelicot')
def coquelicot_download_file(sample_local_file):
file_path = functional.download_file_outside_browser(
sample_local_file['upload_url'])
sample_local_file['download_path'] = file_path
@then('contents of downloaded sample file should be same as sample local file')
def coquelicot_compare_upload_download_files(sample_local_file):
_compare_files(sample_local_file['file_path'],
sample_local_file['download_path'])
def _create_sample_local_file():
"""Create a sample file for upload using browser."""
contents = bytearray(random.getrandbits(8) for _ in range(64))
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(contents)
return temp_file.name, contents
def _verify_upload_password(browser, password):
functional.visit(browser, '/coquelicot')
# ensure the password form is scrolled into view
browser.execute_script('window.scrollTo(100, 0)')
browser.find_by_id('upload_password').fill(password)
actions = ActionChains(browser.driver)
actions.send_keys(Keys.RETURN)
actions.perform()
assert functional.eventually(browser.is_element_present_by_css,
args=['div[style*="display: none;"]'])
def _upload_file(browser, file_path, password):
"""Upload a local file from disk to coquelicot."""
_verify_upload_password(browser, password)
browser.attach_file('file', file_path)
functional.submit(browser)
assert functional.eventually(browser.is_element_present_by_css,
args=['#content .url'])
url_textarea = browser.find_by_css('#content .url textarea').first
return url_textarea.value
def _modify_max_file_size(browser, size):
"""Change the maximum file size of coquelicot to the given value"""
functional.visit(browser, '/plinth/apps/coquelicot/')
browser.find_by_id('id_max_file_size').fill(size)
functional.submit(browser, form_class='form-configuration')
def _get_max_file_size(browser):
"""Get the maximum file size of coquelicot"""
functional.visit(browser, '/plinth/apps/coquelicot/')
return int(browser.find_by_id('id_max_file_size').value)
def _modify_upload_password(browser, password):
"""Change the upload password for coquelicot to the given value"""
functional.visit(browser, '/plinth/apps/coquelicot/')
browser.find_by_id('id_upload_password').fill(password)
functional.submit(browser, form_class='form-configuration')
def _compare_files(file1, file2):
"""Assert that the contents of two files are the same."""
file1_contents = open(file1, 'rb').read()
file2_contents = open(file2, 'rb').read()
assert file1_contents == file2_contents

View File

@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
@essential @date_and_time @system
@essential @datetime @system
Feature: Date and Time
Configure time zone and network time service.
@ -8,16 +8,16 @@ Background:
Given I'm a logged in user
Scenario: Disable network time application
Given the network time application can be disabled
And the network time application is enabled
When I disable the network time application
Then the network time service should not be running
Given the datetime application can be disabled
And the datetime application is enabled
When I disable the datetime application
Then the datetime service should not be running
Scenario: Enable network time application
Given the network time application can be disabled
And the network time application is disabled
When I enable the network time application
Then the network time service should be running
Given the datetime application can be disabled
And the datetime application is disabled
When I enable the datetime application
Then the datetime service should be running
Scenario: Set timezone
When I set the time zone to Africa/Abidjan

View File

@ -3,6 +3,31 @@
Functional, browser based tests for datetime app.
"""
from pytest_bdd import scenarios
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('datetime.feature')
@when(parsers.parse('I set the time zone to {time_zone:S}'))
def time_zone_set(session_browser, time_zone):
_time_zone_set(session_browser, time_zone)
@then(parsers.parse('the time zone should be {time_zone:S}'))
def time_zone_assert(session_browser, time_zone):
assert time_zone == _time_zone_get(session_browser)
def _time_zone_set(browser, time_zone):
"""Set the system time zone."""
functional.nav_to_module(browser, 'datetime')
browser.select('time_zone', time_zone)
functional.submit(browser, form_class='form-configuration')
def _time_zone_get(browser):
"""Set the system time zone."""
functional.nav_to_module(browser, 'datetime')
return browser.find_by_name('time_zone').first.value

View File

@ -3,6 +3,172 @@
Functional, browser based tests for deluge app.
"""
from pytest_bdd import scenarios
import os
import time
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('deluge.feature')
@when('all torrents are removed from deluge')
def deluge_remove_all_torrents(session_browser):
_remove_all_torrents(session_browser)
@when('I upload a sample torrent to deluge')
def deluge_upload_sample_torrent(session_browser):
_upload_sample_torrent(session_browser)
@then(
parsers.parse(
'there should be {torrents_number:d} torrents listed in deluge'))
def deluge_assert_number_of_torrents(session_browser, torrents_number):
assert torrents_number == _get_number_of_torrents(session_browser)
def _get_active_window_title(browser):
"""Return the title of the currently active window in Deluge."""
return browser.evaluate_script(
'Ext.WindowMgr.getActive() ? Ext.WindowMgr.getActive().title : null')
def _ensure_logged_in(browser):
"""Ensure that password dialog is answered and we can interact."""
url = functional.base_url + '/deluge'
def service_is_available():
if browser.is_element_present_by_xpath(
'//h1[text()="Service Unavailable"]'):
functional.access_url(browser, 'deluge')
return False
return True
if browser.url != url:
browser.visit(url)
# After a backup restore, service may not be available immediately
functional.eventually(service_is_available)
time.sleep(1) # Wait for Ext.js application in initialize
if _get_active_window_title(browser) != 'Login':
return
browser.find_by_id('_password').first.fill('deluge')
_click_active_window_button(browser, 'Login')
assert functional.eventually(
lambda: _get_active_window_title(browser) != 'Login')
functional.eventually(browser.is_element_not_present_by_css,
args=['#add.x-item-disabled'], timeout=0.3)
def _open_connection_manager(browser):
"""Open the connection manager dialog if not already open."""
title = 'Connection Manager'
if _get_active_window_title(browser) == title:
return
browser.find_by_css('button.x-deluge-connection-manager').first.click()
functional.eventually(lambda: _get_active_window_title(browser) == title)
def _ensure_connected(browser):
"""Type the connection password if required and start Deluge daemon."""
_ensure_logged_in(browser)
# Change Default Password window appears once.
if _get_active_window_title(browser) == 'Change Default Password':
_click_active_window_button(browser, 'No')
assert functional.eventually(browser.is_element_not_present_by_css,
args=['#add.x-item-disabled'])
def _remove_all_torrents(browser):
"""Remove all torrents from deluge."""
_ensure_connected(browser)
while browser.find_by_css('#torrentGrid .torrent-name'):
browser.find_by_css('#torrentGrid .torrent-name').first.click()
# Click remove toolbar button
browser.find_by_id('remove').first.click()
# Remove window shows up
assert functional.eventually(
lambda: _get_active_window_title(browser) == 'Remove Torrent')
_click_active_window_button(browser, 'Remove With Data')
# Remove window disappears
assert functional.eventually(
lambda: not _get_active_window_title(browser))
def _get_active_window_id(browser):
"""Return the ID of the currently active window."""
return browser.evaluate_script('Ext.WindowMgr.getActive().id')
def _click_active_window_button(browser, button_text):
"""Click an action button in the active window."""
browser.execute_script('''
active_window = Ext.WindowMgr.getActive();
active_window.buttons.forEach(function (button) {{
if (button.text == "{button_text}")
button.btnEl.dom.click()
}})'''.format(button_text=button_text))
def _upload_sample_torrent(browser):
"""Upload a sample torrent into deluge."""
_ensure_connected(browser)
number_of_torrents = _get_number_of_torrents(browser)
# Click add toolbar button
browser.find_by_id('add').first.click()
# Add window appears
functional.eventually(
lambda: _get_active_window_title(browser) == 'Add Torrents')
file_path = os.path.join(os.path.dirname(__file__), 'data',
'sample.torrent')
if browser.find_by_id('fileUploadForm'): # deluge-web 2.x
browser.attach_file('file', file_path)
else: # deluge-web 1.x
browser.find_by_css('button.x-deluge-add-file').first.click()
# Add from file window appears
functional.eventually(
lambda: _get_active_window_title(browser) == 'Add from File')
# Attach file
browser.attach_file('file', file_path)
# Click Add
_click_active_window_button(browser, 'Add')
functional.eventually(
lambda: _get_active_window_title(browser) == 'Add Torrents')
# Click Add
time.sleep(1)
_click_active_window_button(browser, 'Add')
functional.eventually(
lambda: _get_number_of_torrents(browser) > number_of_torrents)
def _get_number_of_torrents(browser):
"""Return the number torrents currently in deluge."""
_ensure_connected(browser)
return len(browser.find_by_css('#torrentGrid .torrent-name'))

View File

@ -3,6 +3,71 @@
Functional, browser based tests for dynamicdns app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, scenarios, then, when
from plinth.tests import functional
scenarios('dynamicdns.feature')
@given('dynamicdns is configured')
def dynamicdns_configure(session_browser):
_configure(session_browser)
@when('I change the dynamicdns configuration')
def dynamicdns_change_config(session_browser):
_change_config(session_browser)
@then('dynamicdns should have the original configuration')
def dynamicdns_has_original_config(session_browser):
assert _has_original_config(session_browser)
def _configure(browser):
functional.nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
browser.find_by_id('id_enabled').check()
browser.find_by_id('id_service_type').select('GnuDIP')
browser.find_by_id('id_dynamicdns_server').fill('example.com')
browser.find_by_id('id_dynamicdns_domain').fill('freedombox.example.com')
browser.find_by_id('id_dynamicdns_user').fill('tester')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip.datasystems24.de')
functional.submit(browser)
def _has_original_config(browser):
functional.nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
enabled = browser.find_by_id('id_enabled').value
service_type = browser.find_by_id('id_service_type').value
server = browser.find_by_id('id_dynamicdns_server').value
domain = browser.find_by_id('id_dynamicdns_domain').value
user = browser.find_by_id('id_dynamicdns_user').value
ipurl = browser.find_by_id('id_dynamicdns_ipurl').value
if enabled and service_type == 'GnuDIP' and server == 'example.com' \
and domain == 'freedombox.example.com' and user == 'tester' \
and ipurl == 'http://myip.datasystems24.de':
return True
else:
return False
def _change_config(browser):
functional.nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
browser.find_by_id('id_enabled').check()
browser.find_by_id('id_service_type').select('GnuDIP')
browser.find_by_id('id_dynamicdns_server').fill('2.example.com')
browser.find_by_id('id_dynamicdns_domain').fill('freedombox2.example.com')
browser.find_by_id('id_dynamicdns_user').fill('tester2')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting2')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip2.datasystems24.de')
functional.submit(browser)

View File

@ -3,6 +3,90 @@
Functional, browser based tests for ejabberd app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('ejabberd.feature')
@given('I have added a contact to my roster')
def ejabberd_add_contact(session_browser):
_jsxc_add_contact(session_browser)
@when('I delete the contact from my roster')
def ejabberd_delete_contact(session_browser):
_jsxc_delete_contact(session_browser)
@then('I should have a contact on my roster')
def ejabberd_should_have_contact(session_browser):
assert functional.eventually(_jsxc_has_contact, [session_browser])
@when(parsers.parse('I enable message archive management'))
def ejabberd_enable_archive_management(session_browser):
_enable_message_archive_management(session_browser)
@when(parsers.parse('I disable message archive management'))
def ejabberd_disable_archive_management(session_browser):
_disable_message_archive_management(session_browser)
def _enable_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
functional.nav_to_module(browser, 'ejabberd')
functional.change_checkbox_status(browser, 'ejabberd', 'id_MAM_enabled',
'enabled')
def _disable_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
functional.nav_to_module(browser, 'ejabberd')
functional.change_checkbox_status(browser, 'ejabberd', 'id_MAM_enabled',
'disabled')
def _jsxc_login(browser):
"""Login to JSXC."""
username = functional.config['DEFAULT']['username']
password = functional.config['DEFAULT']['password']
functional.access_url(browser, 'jsxc')
browser.find_by_id('jsxc-username').fill(username)
browser.find_by_id('jsxc-password').fill(password)
browser.find_by_id('jsxc-submit').click()
relogin = browser.find_by_text('relogin')
if relogin:
relogin.first.click()
browser.find_by_id('jsxc_username').fill(username)
browser.find_by_id('jsxc_password').fill(password)
browser.find_by_text('Connect').first.click()
def _jsxc_add_contact(browser):
"""Add a contact to JSXC user's roster."""
functional.set_domain_name(browser, 'localhost')
functional.install(browser, 'jsxc')
_jsxc_login(browser)
new = browser.find_by_text('new contact')
if new: # roster is empty
new.first.click()
browser.find_by_id('jsxc_username').fill('alice@localhost')
browser.find_by_text('Add').first.click()
def _jsxc_delete_contact(browser):
"""Delete the contact from JSXC user's roster."""
_jsxc_login(browser)
browser.find_by_css('div.jsxc_more').first.click()
browser.find_by_text('delete contact').first.click()
browser.find_by_text('Remove').first.click()
def _jsxc_has_contact(browser):
"""Check whether the contact is in JSXC user's roster."""
_jsxc_login(browser)
contact = browser.find_by_text('alice@localhost')
return bool(contact)

View File

@ -3,6 +3,311 @@
Functional, browser based tests for gitweb app.
"""
from pytest_bdd import scenarios
import contextlib
import os
import shutil
import subprocess
import tempfile
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('gitweb.feature')
_default_url = functional.config['DEFAULT']['url']
@given('a public repository')
@given('a repository')
@given('at least one repository exists')
def gitweb_repo(session_browser):
_create_repo(session_browser, 'Test-repo', 'public', True)
@given('a private repository')
def gitweb_private_repo(session_browser):
_create_repo(session_browser, 'Test-repo', 'private', True)
@given('both public and private repositories exist')
def gitweb_public_and_private_repo(session_browser):
_create_repo(session_browser, 'Test-repo', 'public', True)
_create_repo(session_browser, 'Test-repo2', 'private', True)
@given(parsers.parse("a {access:w} repository that doesn't exist"))
def gitweb_nonexistent_repo(session_browser, access):
_delete_repo(session_browser, 'Test-repo', ignore_missing=True)
return dict(access=access)
@given('all repositories are private')
def gitweb_all_repositories_private(session_browser):
_set_all_repos_private(session_browser)
@given(parsers.parse('a repository metadata:\n{metadata}'))
def gitweb_repo_metadata(session_browser, metadata):
metadata_dict = {}
for item in metadata.split('\n'):
item = item.split(': ')
metadata_dict[item[0]] = item[1]
return metadata_dict
@when('I create the repository')
def gitweb_create_repo(session_browser, access):
_create_repo(session_browser, 'Test-repo', access)
@when('I delete the repository')
def gitweb_delete_repo(session_browser):
_delete_repo(session_browser, 'Test-repo')
@when('I set the metadata of the repository')
def gitweb_edit_repo_metadata(session_browser, gitweb_repo_metadata):
_edit_repo_metadata(session_browser, 'Test-repo', gitweb_repo_metadata)
@when('using a git client')
def gitweb_using_git_client():
pass
@then('the repository should be restored')
@then('the repository should be listed as a public')
def gitweb_repo_should_exists(session_browser):
assert _repo_exists(session_browser, 'Test-repo', access='public')
@then('the repository should be listed as a private')
def gitweb_private_repo_should_exists(session_browser):
assert _repo_exists(session_browser, 'Test-repo', 'private')
@then('the repository should not be listed')
def gitweb_repo_should_not_exist(session_browser, gitweb_repo):
assert not _repo_exists(session_browser, gitweb_repo)
@then('the public repository should be listed on gitweb')
@then('the repository should be listed on gitweb')
def gitweb_repo_should_exist_on_gitweb(session_browser):
assert _site_repo_exists(session_browser, 'Test-repo')
@then('the private repository should not be listed on gitweb')
def gitweb_private_repo_should_exists_on_gitweb(session_browser):
assert not _site_repo_exists(session_browser, 'Test-repo2')
@then('the metadata of the repository should be as set')
def gitweb_repo_metadata_should_match(session_browser, gitweb_repo_metadata):
actual_metadata = _get_repo_metadata(session_browser, 'Test-repo')
assert all(item in actual_metadata.items()
for item in gitweb_repo_metadata.items())
@then('the repository should be publicly readable')
def gitweb_repo_publicly_readable():
assert _repo_is_readable('Test-repo')
assert _repo_is_readable('Test-repo', url_git_extension=True)
@then('the repository should not be publicly readable')
def gitweb_repo_not_publicly_readable():
assert not _repo_is_readable('Test-repo')
assert not _repo_is_readable('Test-repo', url_git_extension=True)
@then('the repository should not be publicly writable')
def gitweb_repo_not_publicly_writable():
assert not _repo_is_writable('Test-repo')
assert not _repo_is_writable('Test-repo', url_git_extension=True)
@then('the repository should be privately readable')
def gitweb_repo_privately_readable():
assert _repo_is_readable('Test-repo', with_auth=True)
assert _repo_is_readable('Test-repo', with_auth=True,
url_git_extension=True)
@then('the repository should be privately writable')
def gitweb_repo_privately_writable():
assert _repo_is_writable('Test-repo', with_auth=True)
assert _repo_is_writable('Test-repo', with_auth=True,
url_git_extension=True)
def _create_repo(browser, repo, access=None, ok_if_exists=False):
"""Create repository."""
if not _repo_exists(browser, repo, access):
_delete_repo(browser, repo, ignore_missing=True)
browser.find_link_by_href('/plinth/apps/gitweb/create/').first.click()
browser.find_by_id('id_gitweb-name').fill(repo)
if access == 'private':
browser.find_by_id('id_gitweb-is_private').check()
elif access == 'public':
browser.find_by_id('id_gitweb-is_private').uncheck()
functional.submit(browser)
elif not ok_if_exists:
assert False, 'Repo already exists.'
def _delete_repo(browser, repo, ignore_missing=False):
"""Delete repository."""
functional.nav_to_module(browser, 'gitweb')
delete_link = browser.find_link_by_href(
'/plinth/apps/gitweb/{}/delete/'.format(repo))
if delete_link or not ignore_missing:
delete_link.first.click()
functional.submit(browser)
def _edit_repo_metadata(browser, repo, metadata):
"""Set repository metadata."""
functional.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
if 'name' in metadata:
browser.find_by_id('id_gitweb-name').fill(metadata['name'])
if 'description' in metadata:
browser.find_by_id('id_gitweb-description').fill(
metadata['description'])
if 'owner' in metadata:
browser.find_by_id('id_gitweb-owner').fill(metadata['owner'])
if 'access' in metadata:
if metadata['access'] == 'private':
browser.find_by_id('id_gitweb-is_private').check()
else:
browser.find_by_id('id_gitweb-is_private').uncheck()
functional.submit(browser)
def _get_repo_metadata(browser, repo):
"""Get repository metadata."""
functional.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
metadata = {}
for item in ['name', 'description', 'owner']:
metadata[item] = browser.find_by_id('id_gitweb-' + item).value
if browser.find_by_id('id_gitweb-is_private').value:
metadata['access'] = 'private'
else:
metadata['access'] = 'public'
return metadata
def _get_repo_url(repo, with_auth):
""""Get repository URL"""
scheme = 'http'
if _default_url.startswith('https://'):
scheme = 'https'
url = _default_url.split(
'://')[1] if '://' in _default_url else _default_url
password = 'gitweb_wrong_password'
if with_auth:
password = functional.config['DEFAULT']['password']
return '{0}://{1}:{2}@{3}/gitweb/{4}'.format(
scheme, functional.config['DEFAULT']['username'], password, url, repo)
@contextlib.contextmanager
def _gitweb_temp_directory():
"""Create temporary directory"""
name = tempfile.mkdtemp(prefix='plinth_test_gitweb_')
yield name
shutil.rmtree(name)
def _gitweb_git_command_is_successful(command, cwd):
"""Check if a command runs successfully or gives authentication error"""
process = subprocess.run(command, capture_output=True, cwd=cwd)
if process.returncode != 0:
if 'Authentication failed' in process.stderr.decode():
return False
print(process.stdout.decode())
# raise exception
process.check_returncode()
return True
def _repo_exists(browser, repo, access=None):
"""Check whether the repository exists."""
functional.nav_to_module(browser, 'gitweb')
links_found = browser.find_link_by_href('/gitweb/{}.git'.format(repo))
access_matches = True
if links_found and access:
parent = links_found.first.find_by_xpath('..').first
private_icon = parent.find_by_css('.repo-private-icon')
if access == 'private':
access_matches = bool(private_icon)
if access == 'public':
access_matches = not bool(private_icon)
return bool(links_found) and access_matches
def _repo_is_readable(repo, with_auth=False, url_git_extension=False):
"""Check if a git repo is readable with git client."""
url = _get_repo_url(repo, with_auth)
if url_git_extension:
url = url + '.git'
git_command = ['git', 'clone', '-c', 'http.sslverify=false', url]
with _gitweb_temp_directory() as cwd:
return _gitweb_git_command_is_successful(git_command, cwd)
def _repo_is_writable(repo, with_auth=False, url_git_extension=False):
"""Check if a git repo is writable with git client."""
url = _get_repo_url(repo, with_auth)
if url_git_extension:
url = url + '.git'
with _gitweb_temp_directory() as cwd:
subprocess.run(['mkdir', 'test-project'], check=True, cwd=cwd)
cwd = os.path.join(cwd, 'test-project')
prepare_git_repo_commands = [
'git init -q', 'git config http.sslVerify false',
'git -c "user.name=Tester" -c "user.email=tester" '
'commit -q --allow-empty -m "test"'
]
for command in prepare_git_repo_commands:
subprocess.run(command, shell=True, check=True, cwd=cwd)
git_push_command = ['git', 'push', '-qf', url, 'master']
return _gitweb_git_command_is_successful(git_push_command, cwd)
def _set_repo_access(browser, repo, access):
"""Set repository as public or private."""
functional.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
if access == 'private':
browser.find_by_id('id_gitweb-is_private').check()
else:
browser.find_by_id('id_gitweb-is_private').uncheck()
functional.submit(browser)
def _set_all_repos_private(browser):
"""Set all repositories private"""
functional.nav_to_module(browser, 'gitweb')
public_repos = []
for element in browser.find_by_css('#gitweb-repo-list .list-group-item'):
if not element.find_by_css('.repo-private-icon'):
repo = element.find_by_css('.repo-label').first.text
public_repos.append(repo)
for repo in public_repos:
_set_repo_access(browser, repo, 'private')
def _site_repo_exists(browser, repo):
"""Check whether the repository exists on Gitweb site."""
browser.visit('{}/gitweb'.format(_default_url))
return browser.find_by_css('a[href="/gitweb/{0}.git"]'.format(repo))

View File

@ -3,6 +3,26 @@
Functional, browser based tests for help app.
"""
from pytest_bdd import scenarios
from pytest_bdd import scenarios, then, when
from plinth.tests import functional
scenarios('help.feature')
@when('I go to the status logs page')
def help_go_to_status_logs(session_browser):
_go_to_status_logs(session_browser)
@then('status logs should be shown')
def help_status_logs_are_shown(session_browser):
assert _are_status_logs_shown(session_browser)
def _go_to_status_logs(browser):
functional.visit(browser, '/plinth/help/status-log/')
def _are_status_logs_shown(browser):
return browser.is_text_present('Logs begin')

View File

@ -6,23 +6,23 @@ Feature: ikiwiki Wiki and Blog
Background:
Given I'm a logged in user
Given the wiki application is installed
Given the ikiwiki application is installed
Scenario: Enable wiki application
Given the wiki application is disabled
When I enable the wiki application
Then the wiki site should be available
Scenario: Enable ikiwiki application
Given the ikiwiki application is disabled
When I enable the ikiwiki application
Then the ikiwiki site should be available
@backups
Scenario: Backup and restore wiki
Given the wiki application is enabled
Scenario: Backup and restore ikiwiki
Given the ikiwiki application is enabled
When there is an ikiwiki wiki
And I create a backup of the ikiwiki app data with name test_ikiwiki
And I delete the ikiwiki wiki
And I restore the ikiwiki app data backup with name test_ikiwiki
Then the ikiwiki wiki should be restored
Scenario: Disable wiki application
Given the wiki application is enabled
When I disable the wiki application
Then the wiki site should not be available
Scenario: Disable ikiwiki application
Given the ikiwiki application is enabled
When I disable the ikiwiki application
Then the ikiwiki site should not be available

View File

@ -3,6 +3,52 @@
Functional, browser based tests for ikiwiki app.
"""
from pytest_bdd import scenarios
from pytest_bdd import scenarios, then, when
from plinth.tests import functional
scenarios('ikiwiki.feature')
@when('there is an ikiwiki wiki')
def ikiwiki_create_wiki_if_needed(session_browser):
_create_wiki_if_needed(session_browser)
@when('I delete the ikiwiki wiki')
def ikiwiki_delete_wiki(session_browser):
_delete_wiki(session_browser)
@then('the ikiwiki wiki should be restored')
def ikiwiki_should_exist(session_browser):
assert _wiki_exists(session_browser)
def _create_wiki_if_needed(browser):
"""Create wiki if it does not exist."""
functional.nav_to_module(browser, 'ikiwiki')
wiki = browser.find_link_by_href('/ikiwiki/wiki')
if not wiki:
browser.find_link_by_href('/plinth/apps/ikiwiki/create/').first.click()
browser.find_by_id('id_ikiwiki-name').fill('wiki')
browser.find_by_id('id_ikiwiki-admin_name').fill(
functional.config['DEFAULT']['username'])
browser.find_by_id('id_ikiwiki-admin_password').fill(
functional.config['DEFAULT']['password'])
functional.submit(browser)
def _delete_wiki(browser):
"""Delete wiki."""
functional.nav_to_module(browser, 'ikiwiki')
browser.find_link_by_href(
'/plinth/apps/ikiwiki/wiki/delete/').first.click()
functional.submit(browser)
def _wiki_exists(browser):
"""Check whether the wiki exists."""
functional.nav_to_module(browser, 'ikiwiki')
wiki = browser.find_link_by_href('/ikiwiki/wiki')
return bool(wiki)

View File

@ -3,6 +3,216 @@
Functional, browser based tests for mediawiki app.
"""
from pytest_bdd import scenarios
import pathlib
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('mediawiki.feature')
@when(parsers.parse('I enable mediawiki public registrations'))
def enable_mediawiki_public_registrations(session_browser):
_enable_public_registrations(session_browser)
@when(parsers.parse('I disable mediawiki public registrations'))
def disable_mediawiki_public_registrations(session_browser):
_disable_public_registrations(session_browser)
@when(parsers.parse('I enable mediawiki private mode'))
def enable_mediawiki_private_mode(session_browser):
_enable_private_mode(session_browser)
@when(parsers.parse('I disable mediawiki private mode'))
def disable_mediawiki_private_mode(session_browser):
_disable_private_mode(session_browser)
@when(parsers.parse('I set the mediawiki admin password to {password}'))
def set_mediawiki_admin_password(session_browser, password):
_set_admin_password(session_browser, password)
@then(parsers.parse('the mediawiki site should allow creating accounts'))
def mediawiki_allows_creating_accounts(session_browser):
_verify_create_account_link(session_browser)
@then(parsers.parse('the mediawiki site should not allow creating accounts'))
def mediawiki_does_not_allow_creating_accounts(session_browser):
_verify_no_create_account_link(session_browser)
@then(
parsers.parse('the mediawiki site should allow anonymous reads and writes')
)
def mediawiki_allows_anonymous_reads_edits(session_browser):
_verify_anonymous_reads_edits_link(session_browser)
@then(
parsers.parse(
'the mediawiki site should not allow anonymous reads and writes'))
def mediawiki_does_not_allow__account_creation_anonymous_reads_edits(
session_browser):
_verify_no_anonymous_reads_edits_link(session_browser)
@then(
parsers.parse(
'I should see the Upload File option in the side pane when logged in '
'with credentials {username:w} and {password:w}'))
def login_to_mediawiki_with_credentials(session_browser, username, password):
_login_with_credentials(session_browser, username, password)
@when('I delete the mediawiki main page')
def mediawiki_delete_main_page(session_browser):
_delete_main_page(session_browser)
@then('the mediawiki main page should be restored')
def mediawiki_verify_text(session_browser):
assert _has_main_page(session_browser)
@when(
parsers.parse(
'I upload an image named {image:S} to mediawiki with credentials '
'{username:w} and {password:w}'))
def upload_image(session_browser, username, password, image):
_upload_image(session_browser, username, password, image)
@then(parsers.parse('there should be {image:S} image'))
def uploaded_image_should_be_available(session_browser, image):
uploaded_image = _get_uploaded_image(session_browser, image)
assert image.lower() == uploaded_image.lower()
def _enable_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
functional.nav_to_module(browser, 'mediawiki')
functional.change_checkbox_status(browser, 'mediawiki',
'id_enable_public_registrations',
'enabled')
def _disable_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
functional.nav_to_module(browser, 'mediawiki')
functional.change_checkbox_status(browser, 'mediawiki',
'id_enable_public_registrations',
'disabled')
def _enable_private_mode(browser):
"""Enable public registrations in MediaWiki."""
functional.nav_to_module(browser, 'mediawiki')
functional.change_checkbox_status(browser, 'mediawiki',
'id_enable_private_mode', 'enabled')
def _disable_private_mode(browser):
"""Enable public registrations in MediaWiki."""
functional.nav_to_module(browser, 'mediawiki')
functional.change_checkbox_status(browser, 'mediawiki',
'id_enable_private_mode', 'disabled')
def _set_admin_password(browser, password):
"""Set a password for the MediaWiki user called admin."""
functional.nav_to_module(browser, 'mediawiki')
browser.find_by_id('id_password').fill(password)
functional.submit(browser, form_class='form-configuration')
def _verify_create_account_link(browser):
functional.visit(browser, '/mediawiki/index.php/Special:CreateAccount')
assert functional.eventually(browser.is_element_present_by_id,
args=['wpCreateaccount'])
def _verify_no_create_account_link(browser):
functional.visit(browser, '/mediawiki/index.php/Special:CreateAccount')
assert functional.eventually(browser.is_element_not_present_by_id,
args=['wpCreateaccount'])
def _verify_anonymous_reads_edits_link(browser):
functional.visit(browser, '/mediawiki')
assert functional.eventually(browser.is_element_present_by_id,
args=['ca-nstab-main'])
def _verify_no_anonymous_reads_edits_link(browser):
functional.visit(browser, '/mediawiki')
assert functional.eventually(browser.is_element_not_present_by_id,
args=['ca-nstab-main'])
assert functional.eventually(browser.is_element_present_by_id,
args=['ca-nstab-special'])
def _login(browser, username, password):
functional.visit(browser, '/mediawiki/index.php?title=Special:Login')
browser.find_by_id('wpName1').fill(username)
browser.find_by_id('wpPassword1').fill(password)
with functional.wait_for_page_update(browser):
browser.find_by_id('wpLoginAttempt').click()
def _login_with_credentials(browser, username, password):
_login(browser, username, password)
# Had to put it in the same step because sessions don't
# persist between steps
assert functional.eventually(browser.is_element_present_by_id,
args=['t-upload'])
def _upload_image(browser, username, password, image):
"""Upload an image to MediaWiki. Idempotent."""
functional.visit(browser, '/mediawiki')
_login(browser, username, password)
# Upload file
functional.visit(browser, '/mediawiki/Special:Upload')
file_path = pathlib.Path(__file__).parent
file_path /= '../../../../static/themes/default/img/' + image
browser.attach_file('wpUploadFile', str(file_path.resolve()))
functional.submit(browser, element=browser.find_by_name('wpUpload')[0])
def _get_number_of_uploaded_images(browser):
functional.visit(browser, '/mediawiki/Special:ListFiles')
return len(browser.find_by_css('.TablePager_col_img_timestamp'))
def _get_uploaded_image(browser, image):
functional.visit(browser, '/mediawiki/Special:ListFiles')
elements = browser.find_link_by_partial_href(image)
return elements[0].value
def _delete_main_page(browser):
"""Delete the mediawiki main page."""
_login(browser, 'admin', 'whatever123')
functional.visit(browser,
'/mediawiki/index.php?title=Main_Page&action=delete')
with functional.wait_for_page_update(browser):
browser.find_by_id('wpConfirmB').first.click()
def _has_main_page(browser):
"""Check if mediawiki main page exists."""
return functional.eventually(__has_main_page, [browser])
def __has_main_page(browser):
"""Check if mediawiki main page exists."""
functional.visit(browser, '/mediawiki/Main_Page')
content = browser.find_by_id('mw-content-text').first
return 'This page has been deleted.' not in content.text

View File

@ -3,6 +3,60 @@
Functional, browser based tests for mldonkey app.
"""
from pytest_bdd import scenarios
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('mldonkey.feature')
@when('all ed2k files are removed from mldonkey')
def mldonkey_remove_all_ed2k_files(session_browser):
_remove_all_ed2k_files(session_browser)
@when('I upload a sample ed2k file to mldonkey')
def mldonkey_upload_sample_ed2k_file(session_browser):
_upload_sample_ed2k_file(session_browser)
@then(
parsers.parse(
'there should be {ed2k_files_number:d} ed2k files listed in mldonkey'))
def mldonkey_assert_number_of_ed2k_files(session_browser, ed2k_files_number):
assert ed2k_files_number == _get_number_of_ed2k_files(session_browser)
def _submit_command(browser, command):
"""Submit a command to mldonkey."""
with browser.get_iframe('commands') as commands_frame:
commands_frame.find_by_css('.txt2').fill(command)
commands_frame.find_by_css('.but2').click()
def _remove_all_ed2k_files(browser):
"""Remove all ed2k files from mldonkey."""
functional.visit(browser, '/mldonkey/')
_submit_command(browser, 'cancel all')
_submit_command(browser, 'confirm yes')
def _upload_sample_ed2k_file(browser):
"""Upload a sample ed2k file into mldonkey."""
functional.visit(browser, '/mldonkey/')
dllink_command = 'dllink ed2k://|file|foo.bar|123|' \
'0123456789ABCDEF0123456789ABCDEF|/'
_submit_command(browser, dllink_command)
def _get_number_of_ed2k_files(browser):
"""Return the number of ed2k files currently in mldonkey."""
functional.visit(browser, '/mldonkey/')
with browser.get_iframe('commands') as commands_frame:
commands_frame.find_by_xpath(
'//tr//td[contains(text(), "Transfers")]').click()
with browser.get_iframe('output') as output_frame:
return len(output_frame.find_by_css('.dl-1')) + len(
output_frame.find_by_css('.dl-2'))

View File

@ -3,6 +3,79 @@
Functional, browser based tests for monkeysphere app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('monkeysphere.feature')
@given(
parsers.parse(
'the {key_type:w} key for {domain:S} is imported in monkeysphere'))
def monkeysphere_given_import_key(session_browser, key_type, domain):
_import_key(session_browser, key_type.lower(), domain)
@when(parsers.parse('I import {key_type:w} key for {domain:S} in monkeysphere')
)
def monkeysphere_import_key(session_browser, key_type, domain):
_import_key(session_browser, key_type.lower(), domain)
@then(
parsers.parse(
'the {key_type:w} key should imported for {domain:S} in monkeysphere'))
def monkeysphere_assert_imported_key(session_browser, key_type, domain):
_assert_imported_key(session_browser, key_type.lower(), domain)
@then(
parsers.parse('I should be able to publish {key_type:w} key for '
'{domain:S} in monkeysphere'))
def monkeysphere_publish_key(session_browser, key_type, domain):
_publish_key(session_browser, key_type.lower(), domain)
def _find_domain(browser, key_type, domain_type, domain):
"""Iterate every domain of a given type which given key type."""
keys_of_type = browser.find_by_css(
'.monkeysphere-service-{}'.format(key_type))
for key_of_type in keys_of_type:
search_domains = key_of_type.find_by_css(
'.monkeysphere-{}-domain'.format(domain_type))
for search_domain in search_domains:
if search_domain.text == domain:
return key_of_type, search_domain
raise IndexError('Domain not found')
def _import_key(browser, key_type, domain):
"""Import a key of specified type for given domain into monkeysphere."""
try:
monkeysphere_assert_imported_key(browser, key_type, domain)
except IndexError:
pass
else:
return
key, _ = _find_domain(browser, key_type, 'importable', domain)
with functional.wait_for_page_update(browser):
key.find_by_css('.button-import').click()
def _assert_imported_key(browser, key_type, domain):
"""Assert that a key of specified type for given domain was imported.."""
functional.nav_to_module(browser, 'monkeysphere')
return _find_domain(browser, key_type, 'imported', domain)
def _publish_key(browser, key_type, domain):
"""Publish a key of specified type for given domain from monkeysphere."""
functional.nav_to_module(browser, 'monkeysphere')
key, _ = _find_domain(browser, key_type, 'imported', domain)
with functional.wait_for_page_update(browser):
key.find_by_css('.button-publish').click()
functional.wait_for_config_update(browser, 'monkeysphere')

View File

@ -3,6 +3,44 @@
Functional, browser based tests for openvpn app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then
from plinth.tests import functional
scenarios('openvpn.feature')
@given(parsers.parse('the openvpn application is setup'))
def openvpn_setup(session_browser):
"""Setup the OpenVPN application after installation."""
functional.nav_to_module(session_browser, 'openvpn')
setup_form = session_browser.find_by_css('.form-setup')
if not setup_form:
return
functional.submit(session_browser, form_class='form-setup')
functional.wait_for_config_update(session_browser, 'openvpn')
@given('I download openvpn profile')
def openvpn_download_profile(session_browser):
return _download_profile(session_browser)
@then('the openvpn profile should be downloadable')
def openvpn_profile_downloadable(session_browser):
_download_profile(session_browser)
@then('the openvpn profile downloaded should be same as before')
def openvpn_profile_download_compare(session_browser,
openvpn_download_profile):
new_profile = _download_profile(session_browser)
assert openvpn_download_profile == new_profile
def _download_profile(browser):
"""Download the current user's profile into a file and return path."""
functional.nav_to_module(browser, 'openvpn')
url = browser.find_by_css('.form-profile')['action']
return functional.download_file(browser, url)

View File

@ -3,6 +3,45 @@
Functional, browser based tests for pagekite app.
"""
from pytest_bdd import scenarios
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('pagekite.feature')
@when(
parsers.parse('I configure pagekite with host {host:S}, port {port:d}, '
'kite name {kite_name:S} and kite secret {kite_secret:w}'))
def pagekite_configure(session_browser, host, port, kite_name, kite_secret):
_configure(session_browser, host, port, kite_name, kite_secret)
@then(
parsers.parse(
'pagekite should be configured with host {host:S}, port {port:d}, '
'kite name {kite_name:S} and kite secret {kite_secret:w}'))
def pagekite_assert_configured(session_browser, host, port, kite_name,
kite_secret):
assert (host, port, kite_name,
kite_secret) == _get_configuration(session_browser)
def _configure(browser, host, port, kite_name, kite_secret):
"""Configure pagekite basic parameters."""
functional.nav_to_module(browser, 'pagekite')
# time.sleep(0.250) # Wait for 200ms show animation to complete
browser.fill('pagekite-server_domain', host)
browser.fill('pagekite-server_port', str(port))
browser.fill('pagekite-kite_name', kite_name)
browser.fill('pagekite-kite_secret', kite_secret)
functional.submit(browser, form_class='form-configuration')
def _get_configuration(browser):
"""Return pagekite basic parameters."""
functional.nav_to_module(browser, 'pagekite')
return (browser.find_by_name('pagekite-server_domain').value,
int(browser.find_by_name('pagekite-server_port').value),
browser.find_by_name('pagekite-kite_name').value,
browser.find_by_name('pagekite-kite_secret').value)

View File

@ -3,6 +3,118 @@
Functional, browser based tests for radicale app.
"""
from pytest_bdd import scenarios
import logging
import requests
from pytest_bdd import given, scenarios, then, when
from plinth.tests import functional
logger = logging.getLogger(__name__)
scenarios('radicale.feature')
@given('the access rights are set to "only the owner can view or make changes"'
)
def radicale_given_owner_only(session_browser):
_set_access_rights(session_browser, 'owner_only')
@given('the access rights are set to "any user can view, but only the '
'owner can make changes"')
def radicale_given_owner_write(session_browser):
_set_access_rights(session_browser, 'owner_write')
@given('the access rights are set to "any user can view or make changes"')
def radicale_given_authenticated(session_browser):
_set_access_rights(session_browser, 'authenticated')
@when('I change the access rights to "only the owner can view or make changes"'
)
def radicale_set_owner_only(session_browser):
_set_access_rights(session_browser, 'owner_only')
@when('I change the access rights to "any user can view, but only the '
'owner can make changes"')
def radicale_set_owner_write(session_browser):
_set_access_rights(session_browser, 'owner_write')
@when('I change the access rights to "any user can view or make changes"')
def radicale_set_authenticated(session_browser):
_set_access_rights(session_browser, 'authenticated')
@then('the access rights should be "only the owner can view or make changes"')
def radicale_check_owner_only(session_browser):
assert _get_access_rights(session_browser) == 'owner_only'
@then('the access rights should be "any user can view, but only the '
'owner can make changes"')
def radicale_check_owner_write(session_browser):
assert _get_access_rights(session_browser) == 'owner_write'
@then('the access rights should be "any user can view or make changes"')
def radicale_check_authenticated(session_browser):
assert _get_access_rights(session_browser) == 'authenticated'
@then('the calendar should be available')
def assert_calendar_is_available(session_browser):
assert _calendar_is_available(session_browser)
@then('the calendar should not be available')
def assert_calendar_is_not_available(session_browser):
assert not _calendar_is_available(session_browser)
@then('the addressbook should be available')
def assert_addressbook_is_available(session_browser):
assert _addressbook_is_available(session_browser)
@then('the addressbook should not be available')
def assert_addressbook_is_not_available(session_browser):
assert not _addressbook_is_available(session_browser)
def _get_access_rights(browser):
access_rights_types = ['owner_only', 'owner_write', 'authenticated']
functional.nav_to_module(browser, 'radicale')
for access_rights_type in access_rights_types:
if browser.find_by_value(access_rights_type).checked:
return access_rights_type
def _set_access_rights(browser, access_rights_type):
functional.nav_to_module(browser, 'radicale')
browser.choose('access_rights', access_rights_type)
functional.submit(browser, form_class='form-configuration')
def _calendar_is_available(browser):
"""Return whether calendar is available at well-known URL."""
conf = functional.config['DEFAULT']
url = functional.base_url + '/.well-known/caldav'
logging.captureWarnings(True)
request = requests.get(url, auth=(conf['username'], conf['password']),
verify=False)
logging.captureWarnings(False)
return request.status_code != 404
def _addressbook_is_available(browser):
"""Return whether addressbook is available at well-known URL."""
conf = functional.config['DEFAULT']
url = functional.base_url + '/.well-known/carddav'
logging.captureWarnings(True)
request = requests.get(url, auth=(conf['username'], conf['password']),
verify=False)
logging.captureWarnings(False)
return request.status_code != 404

View File

@ -3,6 +3,117 @@
Functional, browser based tests for samba app.
"""
from pytest_bdd import scenarios
import random
import string
import subprocess
import urllib
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('samba.feature')
@when(parsers.parse('I {task:w} the {share_type:w} samba share'))
def samba_enable_share(session_browser, task, share_type):
if task == 'enable':
_set_share(session_browser, share_type, status='enabled')
elif task == 'disable':
_set_share(session_browser, share_type, status='disabled')
@then(parsers.parse('I can write to the {share_type:w} samba share'))
def samba_share_should_be_writable(share_type):
_assert_share_is_writable(share_type)
@then(parsers.parse('a guest user can write to the {share_type:w} samba share')
)
def samba_share_should_be_writable_to_guest(share_type):
_assert_share_is_writable(share_type, as_guest=True)
@then(
parsers.parse('a guest user can\'t access the {share_type:w} samba share'))
def samba_share_should_not_be_accessible_to_guest(share_type):
_assert_share_is_not_accessible(share_type, as_guest=True)
@then(parsers.parse('the {share_type:w} samba share should not be available'))
def samba_share_should_not_be_available(share_type):
_assert_share_is_not_available(share_type)
def _set_share(browser, share_type, status='enabled'):
"""Enable or disable samba share."""
disk_name = 'disk'
share_type_name = '{0}_share'.format(share_type)
functional.nav_to_module(browser, 'samba')
for elem in browser.find_by_tag('td'):
if elem.text == disk_name:
share_form = elem.find_by_xpath('(..//*)[2]/form').first
share_btn = share_form.find_by_name(share_type_name).first
if status == 'enabled' and share_btn['value'] == 'enable':
share_btn.click()
elif status == 'disabled' and share_btn['value'] == 'disable':
share_btn.click()
break
def _write_to_share(share_type, as_guest=False):
"""Write to the samba share, return output messages as string."""
disk_name = 'disk'
default_url = functional.config['DEFAULT']['url']
if share_type == 'open':
share_name = disk_name
else:
share_name = '{0}_{1}'.format(disk_name, share_type)
hostname = urllib.parse.urlparse(default_url).hostname
servicename = '\\\\{0}\\{1}'.format(hostname, share_name)
directory = '_plinth-test_{0}'.format(''.join(
random.SystemRandom().choices(string.ascii_letters, k=8)))
port = functional.config['DEFAULT']['samba_port']
smb_command = ['smbclient', '-W', 'WORKGROUP', '-p', port]
if as_guest:
smb_command += ['-N']
else:
smb_command += [
'-U', '{0}%{1}'.format(functional.config['DEFAULT']['username'],
functional.config['DEFAULT']['password'])
]
smb_command += [
servicename, '-c', 'mkdir {0}; rmdir {0}'.format(directory)
]
return subprocess.check_output(smb_command).decode()
def _assert_share_is_writable(share_type, as_guest=False):
"""Assert that samba share is writable."""
output = _write_to_share(share_type, as_guest=False)
assert not output, output
def _assert_share_is_not_accessible(share_type, as_guest=False):
"""Assert that samba share is not accessible."""
try:
_write_to_share(share_type, as_guest)
except subprocess.CalledProcessError as err:
err_output = err.output.decode()
assert 'NT_STATUS_ACCESS_DENIED' in err_output, err_output
else:
assert False, 'Can access the share.'
def _assert_share_is_not_available(share_type):
"""Assert that samba share is not accessible."""
try:
_write_to_share(share_type)
except subprocess.CalledProcessError as err:
err_output = err.output.decode()
assert 'NT_STATUS_BAD_NETWORK_NAME' in err_output, err_output
else:
assert False, 'Can access the share.'

View File

@ -3,6 +3,37 @@
Functional, browser based tests for searx app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, scenarios, when
from plinth.tests import functional
scenarios('searx.feature')
@given('public access is enabled in searx')
def searx_public_access_enabled(session_browser):
_enable_public_access(session_browser)
@when('I enable public access in searx')
def searx_enable_public_access(session_browser):
_enable_public_access(session_browser)
@when('I disable public access in searx')
def searx_disable_public_access(session_browser):
_disable_public_access(session_browser)
def _enable_public_access(browser):
"""Enable Public Access in SearX"""
functional.nav_to_module(browser, 'searx')
browser.find_by_id('id_public_access').check()
functional.submit(browser, form_class='form-configuration')
def _disable_public_access(browser):
"""Enable Public Access in SearX"""
functional.nav_to_module(browser, 'searx')
browser.find_by_id('id_public_access').uncheck()
functional.submit(browser, form_class='form-configuration')

View File

@ -3,6 +3,43 @@
Functional, browser based tests for security app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('security.feature')
@given(parsers.parse('restricted console logins are {enabled}'))
def security_given_enable_restricted_logins(session_browser, enabled):
should_enable = (enabled == 'enabled')
_enable_restricted_logins(session_browser, should_enable)
@when(parsers.parse('I {enable} restricted console logins'))
def security_enable_restricted_logins(session_browser, enable):
should_enable = (enable == 'enable')
_enable_restricted_logins(session_browser, should_enable)
@then(parsers.parse('restricted console logins should be {enabled}'))
def security_assert_restricted_logins(session_browser, enabled):
enabled = (enabled == 'enabled')
assert _get_restricted_logins(session_browser) == enabled
def _enable_restricted_logins(browser, should_enable):
"""Enable/disable restricted logins in security module."""
functional.nav_to_module(browser, 'security')
if should_enable:
browser.check('security-restricted_access')
else:
browser.uncheck('security-restricted_access')
functional.submit(browser)
def _get_restricted_logins(browser):
"""Return whether restricted console logins is enabled."""
functional.nav_to_module(browser, 'security')
return browser.find_by_name('security-restricted_access').first.checked

View File

@ -3,6 +3,43 @@
Functional, browser based tests for shadowsocks app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('shadowsocks.feature')
@given('the shadowsocks application is configured')
def configure_shadowsocks(session_browser):
_configure(session_browser, 'example.com', 'fakepassword')
@when(
parsers.parse('I configure shadowsocks with server {server:S} and '
'password {password:w}'))
def configure_shadowsocks_with_details(session_browser, server, password):
_configure(session_browser, server, password)
@then(
parsers.parse('shadowsocks should be configured with server {server:S} '
'and password {password:w}'))
def assert_shadowsocks_configuration(session_browser, server, password):
assert (server, password) == _get_configuration(session_browser)
def _configure(browser, server, password):
"""Configure shadowsocks client with given server details."""
functional.visit(browser, '/plinth/apps/shadowsocks/')
browser.find_by_id('id_server').fill(server)
browser.find_by_id('id_password').fill(password)
functional.submit(browser, form_class='form-configuration')
def _get_configuration(browser):
"""Return the server and password currently configured in shadowsocks."""
functional.visit(browser, '/plinth/apps/shadowsocks/')
server = browser.find_by_id('id_server').value
password = browser.find_by_id('id_password').value
return server, password

View File

@ -3,6 +3,145 @@
Functional, browser based tests for sharing app.
"""
from pytest_bdd import scenarios
import pytest
import splinter
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('sharing.feature')
@given(parsers.parse('share {name:w} is not available'))
def remove_share(session_browser, name):
_remove_share(session_browser, name)
@when(parsers.parse('I add a share {name:w} from path {path} for {group:w}'))
def add_share(session_browser, name, path, group):
_add_share(session_browser, name, path, group)
@when(
parsers.parse('I edit share {old_name:w} to {new_name:w} from path {path} '
'for {group:w}'))
def edit_share(session_browser, old_name, new_name, path, group):
_edit_share(session_browser, old_name, new_name, path, group)
@when(parsers.parse('I remove share {name:w}'))
def remove_share2(session_browser, name):
_remove_share(session_browser, name)
@when(parsers.parse('I edit share {name:w} to be public'))
def edit_share_public_access(session_browser, name):
_make_share_public(session_browser, name)
@then(
parsers.parse(
'the share {name:w} should be listed from path {path} for {group:w}'))
def verify_share(session_browser, name, path, group):
_verify_share(session_browser, name, path, group)
@then(parsers.parse('the share {name:w} should not be listed'))
def verify_invalid_share(session_browser, name):
with pytest.raises(splinter.exceptions.ElementDoesNotExist):
_get_share(session_browser, name)
@then(parsers.parse('the share {name:w} should be accessible'))
def access_share(session_browser, name):
_access_share(session_browser, name)
@then(parsers.parse('the share {name:w} should not exist'))
def verify_nonexistant_share(session_browser, name):
_verify_nonexistant_share(session_browser, name)
@then(parsers.parse('the share {name:w} should not be accessible'))
def verify_inaccessible_share(session_browser, name):
_verify_inaccessible_share(session_browser, name)
def _remove_share(browser, name):
"""Remove a share in sharing app."""
try:
share_row = _get_share(browser, name)
except splinter.exceptions.ElementDoesNotExist:
pass
else:
share_row.find_by_css('.share-remove')[0].click()
def _add_share(browser, name, path, group):
"""Add a share in sharing app."""
functional.visit(browser, '/plinth/apps/sharing/add/')
browser.fill('sharing-name', name)
browser.fill('sharing-path', path)
browser.find_by_css(
'#id_sharing-groups input[value="{}"]'.format(group)).check()
functional.submit(browser)
def _edit_share(browser, old_name, new_name, path, group):
"""Edit a share in sharing app."""
row = _get_share(browser, old_name)
with functional.wait_for_page_update(browser):
row.find_by_css('.share-edit')[0].click()
browser.fill('sharing-name', new_name)
browser.fill('sharing-path', path)
browser.find_by_css('#id_sharing-groups input').uncheck()
browser.find_by_css(
'#id_sharing-groups input[value="{}"]'.format(group)).check()
functional.submit(browser)
def _get_share(browser, name):
"""Return the row for a given share."""
functional.visit(browser, '/plinth/apps/sharing/')
return browser.find_by_id('share-{}'.format(name))[0]
def _verify_share(browser, name, path, group):
"""Verfiy that a share exists in list of shares."""
href = f'{functional.base_url}/share/{name}'
url = f'/share/{name}'
row = _get_share(browser, name)
assert row.find_by_css('.share-name')[0].text == name
assert row.find_by_css('.share-path')[0].text == path
assert row.find_by_css('.share-url a')[0]['href'] == href
assert row.find_by_css('.share-url a')[0].text == url
assert row.find_by_css('.share-groups')[0].text == group
def _access_share(browser, name):
"""Visit a share and see if it is accessible."""
row = _get_share(browser, name)
url = row.find_by_css('.share-url a')[0]['href']
browser.visit(url)
assert '/share/{}'.format(name) in browser.title
def _make_share_public(browser, name):
"""Make share publicly accessible."""
row = _get_share(browser, name)
with functional.wait_for_page_update(browser):
row.find_by_css('.share-edit')[0].click()
browser.find_by_id('id_sharing-is_public').check()
functional.submit(browser)
def _verify_nonexistant_share(browser, name):
"""Verify that given URL for a given share name is a 404."""
functional.visit(browser, f'/share/{name}')
assert '404' in browser.title
def _verify_inaccessible_share(browser, name):
"""Verify that given URL for a given share name denies permission."""
functional.visit(browser, f'/share/{name}')
functional.eventually(lambda: '/plinth' in browser.url, args=[])

View File

@ -3,6 +3,125 @@
Functional, browser based tests for snapshot app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('snapshot.feature')
@given('the list of snapshots is empty')
def empty_snapshots_list(session_browser):
_delete_all(session_browser)
@when('I manually create a snapshot')
def create_snapshot(session_browser):
_create(session_browser)
@then(parsers.parse('there should be {count:d} snapshot in the list'))
def verify_snapshot_count(session_browser, count):
num_snapshots = _get_count(session_browser)
assert num_snapshots == count
@given(
parsers.parse(
'snapshots are configured with free space {free_space:d}, timeline '
'snapshots {timeline_enabled:w}, software snapshots '
'{software_enabled:w}, hourly limit {hourly:d}, daily limit {daily:d}'
', weekly limit {weekly:d}, monthly limit {monthly:d}, yearly limit '
'{yearly:d}'))
def snapshot_given_set_configuration(session_browser, free_space,
timeline_enabled, software_enabled,
hourly, daily, weekly, monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
_set_configuration(session_browser, free_space, timeline_enabled,
software_enabled, hourly, daily, weekly, monthly,
yearly)
@when(
parsers.parse(
'I configure snapshots with free space {free_space:d}, '
'timeline snapshots {timeline_enabled:w}, '
'software snapshots {software_enabled:w}, hourly limit {hourly:d}, '
'daily limit {daily:d}, weekly limit {weekly:d}, monthly limit '
'{monthly:d}, yearly limit {yearly:d}'))
def snapshot_set_configuration(session_browser, free_space, timeline_enabled,
software_enabled, hourly, daily, weekly,
monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
_set_configuration(session_browser, free_space, timeline_enabled,
software_enabled, hourly, daily, weekly, monthly,
yearly)
@then(
parsers.parse(
'snapshots should be configured with free space {free_space:d}, '
'timeline snapshots {timeline_enabled:w}, software snapshots '
'{software_enabled:w}, hourly limit {hourly:d}, daily limit '
'{daily:d}, weekly limit {weekly:d}, monthly limit {monthly:d}, '
'yearly limit {yearly:d}'))
def snapshot_assert_configuration(session_browser, free_space,
timeline_enabled, software_enabled, hourly,
daily, weekly, monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
assert (free_space, timeline_enabled, software_enabled, hourly, daily,
weekly, monthly, yearly) == _get_configuration(session_browser)
def _delete_all(browser):
if _get_count(browser):
browser.find_by_id('select-all').check()
functional.submit(browser, browser.find_by_name('delete_selected'))
confirm_button = browser.find_by_name('delete_confirm')
if confirm_button: # Only if redirected to confirm page
functional.submit(browser, confirm_button)
def _create(browser):
functional.visit(browser, '/plinth/sys/snapshot/manage/')
functional.submit(browser) # Click on 'Create Snapshot'
def _get_count(browser):
functional.visit(browser, '/plinth/sys/snapshot/manage/')
# Subtract 1 for table header
return len(browser.find_by_xpath('//tr')) - 1
def _set_configuration(browser, free_space, timeline_enabled, software_enabled,
hourly, daily, weekly, monthly, yearly):
"""Set the configuration for snapshots."""
functional.nav_to_module(browser, 'snapshot')
browser.find_by_name('free_space').select(free_space / 100)
browser.find_by_name('enable_timeline_snapshots').select(
'yes' if timeline_enabled else 'no')
browser.find_by_name('enable_software_snapshots').select(
'yes' if software_enabled else 'no')
browser.find_by_name('hourly_limit').fill(hourly)
browser.find_by_name('daily_limit').fill(daily)
browser.find_by_name('weekly_limit').fill(weekly)
browser.find_by_name('monthly_limit').fill(monthly)
browser.find_by_name('yearly_limit').fill(yearly)
functional.submit(browser)
def _get_configuration(browser):
"""Return the current configuration for snapshots."""
functional.nav_to_module(browser, 'snapshot')
return (int(float(browser.find_by_name('free_space').value) * 100),
browser.find_by_name('enable_timeline_snapshots').value == 'yes',
browser.find_by_name('enable_software_snapshots').value == 'yes',
int(browser.find_by_name('hourly_limit').value),
int(browser.find_by_name('daily_limit').value),
int(browser.find_by_name('weekly_limit').value),
int(browser.find_by_name('monthly_limit').value),
int(browser.find_by_name('yearly_limit').value))

View File

@ -3,6 +3,23 @@
Functional, browser based tests for storage app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then
from plinth.tests import functional
scenarios('storage.feature')
@then('the root disk should be shown')
def storage_root_disk_is_shown(session_browser):
assert _is_root_disk_shown(session_browser)
@given(parsers.parse("I'm on the {name:w} page"))
def go_to_module(session_browser, name):
functional.nav_to_module(session_browser, name)
def _is_root_disk_shown(browser):
table_cells = browser.find_by_tag('td')
return any(cell.text == '/' for cell in table_cells)

View File

@ -3,6 +3,141 @@
Functional, browser based tests for syncthing app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('syncthing.feature')
@given(parsers.parse('syncthing folder {folder_name:w} is not present'))
def syncthing_folder_not_present(session_browser, folder_name):
if _folder_is_present(session_browser, folder_name):
_remove_folder(session_browser, folder_name)
@given(
parsers.parse(
'folder {folder_path:S} is present as syncthing folder {folder_name:w}'
))
def syncthing_folder_present(session_browser, folder_name, folder_path):
if not _folder_is_present(session_browser, folder_name):
_add_folder(session_browser, folder_name, folder_path)
@when(
parsers.parse(
'I add a folder {folder_path:S} as syncthing folder {folder_name:w}'))
def syncthing_add_folder(session_browser, folder_name, folder_path):
_add_folder(session_browser, folder_name, folder_path)
@when(parsers.parse('I remove syncthing folder {folder_name:w}'))
def syncthing_remove_folder(session_browser, folder_name):
_remove_folder(session_browser, folder_name)
@then(parsers.parse('syncthing folder {folder_name:w} should be present'))
def syncthing_assert_folder_present(session_browser, folder_name):
assert _folder_is_present(session_browser, folder_name)
@then(parsers.parse('syncthing folder {folder_name:w} should not be present'))
def syncthing_assert_folder_not_present(session_browser, folder_name):
assert not _folder_is_present(session_browser, folder_name)
def _load_main_interface(browser):
"""Close the dialog boxes that many popup after visiting the URL."""
functional.access_url(browser, 'syncthing')
def service_is_available():
if browser.is_element_present_by_xpath(
'//h1[text()="Service Unavailable"]'):
functional.access_url(browser, 'syncthing')
return False
return True
# After a backup restore, service may not be available immediately
functional.eventually(service_is_available)
# Wait for javascript loading process to complete
browser.execute_script('''
document.is_ui_online = false;
var old_console_log = console.log;
console.log = function(message) {
old_console_log.apply(null, arguments);
if (message == 'UIOnline') {
document.is_ui_online = true;
console.log = old_console_log;
}
};
''')
functional.eventually(
lambda: browser.evaluate_script('document.is_ui_online'), timeout=5)
# Dismiss the Usage Reporting consent dialog
usage_reporting = browser.find_by_id('ur').first
functional.eventually(lambda: usage_reporting.visible, timeout=2)
if usage_reporting.visible:
yes_xpath = './/button[contains(@ng-click, "declineUR")]'
usage_reporting.find_by_xpath(yes_xpath).first.click()
functional.eventually(lambda: not usage_reporting.visible)
def _folder_is_present(browser, folder_name):
"""Return whether a folder is present in Syncthing."""
_load_main_interface(browser)
folder_names = browser.find_by_css('#folders .panel-title-text span')
folder_names = [folder_name.text for folder_name in folder_names]
return folder_name in folder_names
def _add_folder(browser, folder_name, folder_path):
"""Add a new folder to Synthing."""
_load_main_interface(browser)
add_folder_xpath = '//button[contains(@ng-click, "addFolder")]'
browser.find_by_xpath(add_folder_xpath).click()
folder_dialog = browser.find_by_id('editFolder').first
functional.eventually(lambda: folder_dialog.visible)
browser.find_by_id('folderLabel').fill(folder_name)
browser.find_by_id('folderPath').fill(folder_path)
save_folder_xpath = './/button[contains(@ng-click, "saveFolder")]'
folder_dialog.find_by_xpath(save_folder_xpath).first.click()
functional.eventually(lambda: not folder_dialog.visible)
def _remove_folder(browser, folder_name):
"""Remove a folder from Synthing."""
_load_main_interface(browser)
# Find folder
folder = None
for current_folder in browser.find_by_css('#folders > .panel'):
name = current_folder.find_by_css('.panel-title-text span').first.text
if name == folder_name:
folder = current_folder
break
# Edit folder button
folder.find_by_css('button.panel-heading').first.click()
functional.eventually(lambda: folder.find_by_css('div.collapse.in'))
edit_folder_xpath = './/button[contains(@ng-click, "editFolder")]'
edit_folder_button = folder.find_by_xpath(edit_folder_xpath).first
edit_folder_button.click()
# Edit folder dialog
folder_dialog = browser.find_by_id('editFolder').first
functional.eventually(lambda: folder_dialog.visible)
remove_button_xpath = './/button[contains(@data-target, "remove-folder")]'
folder_dialog.find_by_xpath(remove_button_xpath).first.click()
# Remove confirmation dialog
remove_folder_dialog = browser.find_by_id('remove-folder-confirmation')
functional.eventually(lambda: remove_folder_dialog.visible)
remove_button_xpath = './/button[contains(@ng-click, "deleteFolder")]'
remove_folder_dialog.find_by_xpath(remove_button_xpath).first.click()
functional.eventually(lambda: not folder_dialog.visible)

View File

@ -3,6 +3,72 @@
Functional, browser based tests for tahoe app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('tahoe.feature')
@then(
parsers.parse(
'{domain:S} should be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_introducer(session_browser, domain, introducer_type):
assert _get_introducer(session_browser, domain, introducer_type)
@then(
parsers.parse(
'{domain:S} should not be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_not_introducer(session_browser, domain, introducer_type):
assert not _get_introducer(session_browser, domain, introducer_type)
@given(parsers.parse('{domain:S} is not a tahoe introducer'))
def tahoe_given_remove_introducer(session_browser, domain):
if _get_introducer(session_browser, domain, 'connected'):
_remove_introducer(session_browser, domain)
@when(parsers.parse('I add {domain:S} as a tahoe introducer'))
def tahoe_add_introducer(session_browser, domain):
_add_introducer(session_browser, domain)
@given(parsers.parse('{domain:S} is a tahoe introducer'))
def tahoe_given_add_introducer(session_browser, domain):
if not _get_introducer(session_browser, domain, 'connected'):
_add_introducer(session_browser, domain)
@when(parsers.parse('I remove {domain:S} as a tahoe introducer'))
def tahoe_remove_introducer(session_browser, domain):
_remove_introducer(session_browser, domain)
def _get_introducer(browser, domain, introducer_type):
"""Return an introducer element with a given type from tahoe-lafs."""
functional.nav_to_module(browser, 'tahoe')
css_class = '.{}-introducers .introducer-furl'.format(introducer_type)
for furl in browser.find_by_css(css_class):
if domain in furl.text:
return furl.parent
return None
def _add_introducer(browser, domain):
"""Add a new introducer into tahoe-lafs."""
functional.nav_to_module(browser, 'tahoe')
furl = 'pb://ewe4zdz6kxn7xhuvc7izj2da2gpbgeir@tcp:{}:3456/' \
'fko4ivfwgqvybppwar3uehkx6spaaou7'.format(domain)
browser.fill('pet_name', 'testintroducer')
browser.fill('furl', furl)
functional.submit(browser, form_class='form-add-introducer')
def _remove_introducer(browser, domain):
"""Remove an introducer from tahoe-lafs."""
introducer = _get_introducer(browser, domain, 'connected')
functional.submit(browser, element=introducer.find_by_css('.form-remove'))

View File

@ -3,6 +3,134 @@
Functional, browser based tests for tor app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
_TOR_FEATURE_TO_ELEMENT = {
'relay': 'tor-relay_enabled',
'bridge-relay': 'tor-bridge_relay_enabled',
'hidden-services': 'tor-hs_enabled',
'software': 'tor-apt_transport_tor_enabled'
}
scenarios('tor.feature')
@given(parsers.parse('tor relay is {enabled:w}'))
def tor_given_relay_enable(session_browser, enabled):
_feature_enable(session_browser, 'relay', enabled)
@when(parsers.parse('I {enable:w} tor relay'))
def tor_relay_enable(session_browser, enable):
_feature_enable(session_browser, 'relay', enable)
@then(parsers.parse('tor relay should be {enabled:w}'))
def tor_assert_relay_enabled(session_browser, enabled):
_assert_feature_enabled(session_browser, 'relay', enabled)
@then(parsers.parse('tor {port_name:w} port should be displayed'))
def tor_assert_port_displayed(session_browser, port_name):
assert port_name in _get_relay_ports(session_browser)
@given(parsers.parse('tor bridge relay is {enabled:w}'))
def tor_given_bridge_relay_enable(session_browser, enabled):
_feature_enable(session_browser, 'bridge-relay', enabled)
@when(parsers.parse('I {enable:w} tor bridge relay'))
def tor_bridge_relay_enable(session_browser, enable):
_feature_enable(session_browser, 'bridge-relay', enable)
@then(parsers.parse('tor bridge relay should be {enabled:w}'))
def tor_assert_bridge_relay_enabled(session_browser, enabled):
_assert_feature_enabled(session_browser, 'bridge-relay', enabled)
@given(parsers.parse('tor hidden services are {enabled:w}'))
def tor_given_hidden_services_enable(session_browser, enabled):
_feature_enable(session_browser, 'hidden-services', enabled)
@when(parsers.parse('I {enable:w} tor hidden services'))
def tor_hidden_services_enable(session_browser, enable):
_feature_enable(session_browser, 'hidden-services', enable)
@then(parsers.parse('tor hidden services should be {enabled:w}'))
def tor_assert_hidden_services_enabled(session_browser, enabled):
_assert_feature_enabled(session_browser, 'hidden-services', enabled)
@then(parsers.parse('tor hidden services information should be displayed'))
def tor_assert_hidden_services(session_browser):
_assert_hidden_services(session_browser)
@given(parsers.parse('download software packages over tor is {enabled:w}'))
def tor_given_download_software_over_tor_enable(session_browser, enabled):
_feature_enable(session_browser, 'software', enabled)
@when(parsers.parse('I {enable:w} download software packages over tor'))
def tor_download_software_over_tor_enable(session_browser, enable):
_feature_enable(session_browser, 'software', enable)
@then(
parsers.parse('download software packages over tor should be {enabled:w}'))
def tor_assert_download_software_over_tor(session_browser, enabled):
_assert_feature_enabled(session_browser, 'software', enabled)
def _feature_enable(browser, feature, should_enable):
"""Enable/disable a Tor feature."""
if not isinstance(should_enable, bool):
should_enable = should_enable in ('enable', 'enabled')
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
functional.nav_to_module(browser, 'tor')
checkbox_element = browser.find_by_name(element_name).first
if should_enable == checkbox_element.checked:
return
if should_enable:
if feature == 'bridge-relay':
browser.find_by_name('tor-relay_enabled').first.check()
checkbox_element.check()
else:
checkbox_element.uncheck()
functional.submit(browser, form_class='form-configuration')
functional.wait_for_config_update(browser, 'tor')
def _assert_feature_enabled(browser, feature, enabled):
"""Assert whether Tor relay is enabled or disabled."""
if not isinstance(enabled, bool):
enabled = enabled in ('enable', 'enabled')
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
functional.nav_to_module(browser, 'tor')
assert browser.find_by_name(element_name).first.checked == enabled
def _get_relay_ports(browser):
"""Return the list of ports shown in the relay table."""
functional.nav_to_module(browser, 'tor')
return [
port_name.text
for port_name in browser.find_by_css('.tor-relay-port-name')
]
def _assert_hidden_services(browser):
"""Assert that hidden service information is shown."""
functional.nav_to_module(browser, 'tor')
assert browser.find_by_css('.tor-hs .tor-hs-hostname')

Binary file not shown.

View File

@ -3,6 +3,67 @@
Functional, browser based tests for transmission app.
"""
from pytest_bdd import scenarios
import os
from pytest_bdd import parsers, scenarios, then, when
from plinth.tests import functional
scenarios('transmission.feature')
@when('all torrents are removed from transmission')
def transmission_remove_all_torrents(session_browser):
_remove_all_torrents(session_browser)
@when('I upload a sample torrent to transmission')
def transmission_upload_sample_torrent(session_browser):
_upload_sample_torrent(session_browser)
@then(
parsers.parse(
'there should be {torrents_number:d} torrents listed in transmission'))
def transmission_assert_number_of_torrents(session_browser, torrents_number):
assert torrents_number == _get_number_of_torrents(session_browser)
def _remove_all_torrents(browser):
"""Remove all torrents from transmission."""
functional.visit(browser, '/transmission')
while True:
torrents = browser.find_by_css('#torrent_list .torrent')
if not torrents:
break
torrents.first.click()
functional.eventually(browser.is_element_not_present_by_css,
args=['#toolbar-remove.disabled'])
browser.click_link_by_id('toolbar-remove')
functional.eventually(
browser.is_element_not_present_by_css,
args=['#dialog-container[style="display: none;"]'])
browser.click_link_by_id('dialog_confirm_button')
functional.eventually(browser.is_element_present_by_css,
args=['#toolbar-remove.disabled'])
def _upload_sample_torrent(browser):
"""Upload a sample torrent into transmission."""
functional.visit(browser, '/transmission')
file_path = os.path.join(os.path.dirname(__file__), 'data',
'sample.torrent')
browser.click_link_by_id('toolbar-open')
functional.eventually(browser.is_element_not_present_by_css,
args=['#upload-container[style="display: none;"]'])
browser.attach_file('torrent_files[]', [file_path])
browser.click_link_by_id('upload_confirm_button')
functional.eventually(browser.is_element_present_by_css,
args=['#torrent_list .torrent'])
def _get_number_of_torrents(browser):
"""Return the number torrents currently in transmission."""
functional.visit(browser, '/transmission')
return len(browser.find_by_css('#torrent_list .torrent'))

View File

@ -3,6 +3,73 @@
Functional, browser based tests for ttrss app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, scenarios, then, when
from plinth.tests import functional
scenarios('ttrss.feature')
@given('I subscribe to a feed in ttrss')
def ttrss_subscribe(session_browser):
_subscribe(session_browser)
@when('I unsubscribe from the feed in ttrss')
def ttrss_unsubscribe(session_browser):
_unsubscribe(session_browser)
@then('I should be subscribed to the feed in ttrss')
def ttrss_assert_subscribed(session_browser):
assert _is_subscribed(session_browser)
def _ttrss_load_main_interface(browser):
"""Load the TT-RSS interface."""
functional.access_url(browser, 'ttrss')
overlay = browser.find_by_id('overlay')
functional.eventually(lambda: not overlay.visible)
def _is_feed_shown(browser, invert=False):
return browser.is_text_present('Planet Debian') != invert
def _subscribe(browser):
"""Subscribe to a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
browser.find_by_text('Actions...').click()
browser.find_by_text('Subscribe to feed...').click()
browser.find_by_id('feedDlg_feedUrl').fill(
'https://planet.debian.org/atom.xml')
browser.find_by_text('Subscribe').click()
if browser.is_text_present('You are already subscribed to this feed.'):
browser.find_by_text('Cancel').click()
expand = browser.find_by_css('span.dijitTreeExpandoClosed')
if expand:
expand.first.click()
assert functional.eventually(_is_feed_shown, [browser])
def _unsubscribe(browser):
"""Unsubscribe from a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
expand = browser.find_by_css('span.dijitTreeExpandoClosed')
if expand:
expand.first.click()
browser.find_by_text('Planet Debian').click()
browser.execute_script("quickMenuGo('qmcRemoveFeed')")
prompt = browser.get_alert()
prompt.accept()
assert functional.eventually(_is_feed_shown, [browser, True])
def _is_subscribed(browser):
"""Return whether subscribed to a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
return browser.is_text_present('Planet Debian')

View File

@ -3,6 +3,47 @@
Functional, browser based tests for upgrades app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('upgrades.feature')
@given(parsers.parse('automatic upgrades are {enabled:w}'))
def upgrades_given_enable_automatic(session_browser, enabled):
should_enable = (enabled == 'enabled')
_enable_automatic(session_browser, should_enable)
@when(parsers.parse('I {enable:w} automatic upgrades'))
def upgrades_enable_automatic(session_browser, enable):
should_enable = (enable == 'enable')
_enable_automatic(session_browser, should_enable)
@then(parsers.parse('automatic upgrades should be {enabled:w}'))
def upgrades_assert_automatic(session_browser, enabled):
should_be_enabled = (enabled == 'enabled')
assert _get_automatic(session_browser) == should_be_enabled
def _enable_automatic(browser, should_enable):
"""Enable/disable automatic software upgrades."""
functional.nav_to_module(browser, 'upgrades')
checkbox_element = browser.find_by_name('auto_upgrades_enabled').first
if should_enable == checkbox_element.checked:
return
if should_enable:
checkbox_element.check()
else:
checkbox_element.uncheck()
functional.submit(browser)
def _get_automatic(browser):
"""Return whether automatic software upgrades is enabled."""
functional.nav_to_module(browser, 'upgrades')
return browser.find_by_name('auto_upgrades_enabled').first.checked

View File

@ -3,6 +3,140 @@
Functional, browser based tests for users app.
"""
from pytest_bdd import scenarios
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
scenarios('users.feature')
_language_codes = {
'Deutsch': 'de',
'Nederlands': 'nl',
'Português': 'pt',
'Türkçe': 'tr',
'dansk': 'da',
'español': 'es',
'français': 'fr',
'norsk (bokmål)': 'nb',
'polski': 'pl',
'svenska': 'sv',
'Русский': 'ru',
'తెలుగు': 'te',
'简体中文': 'zh-hans'
}
_config_page_title_language_map = {
'da': 'Generel Konfiguration',
'de': 'Allgemeine Konfiguration',
'es': 'Configuración general',
'fr': 'Configuration générale',
'nb': 'Generelt oppsett',
'nl': 'Algemene Instellingen',
'pl': 'Ustawienia główne',
'pt': 'Configuração Geral',
'ru': 'Общие настройки',
'sv': 'Allmän Konfiguration',
'te': 'సాధారణ ఆకృతీకరణ',
'tr': 'Genel Yapılandırma',
'zh-hans': '常规配置',
}
@given(parsers.parse("the user {name:w} doesn't exist"))
def new_user_does_not_exist(session_browser, name):
_delete_user(session_browser, name)
@given(parsers.parse('the user {name:w} exists'))
def test_user_exists(session_browser, name):
functional.nav_to_module(session_browser, 'users')
user_link = session_browser.find_link_by_href('/plinth/sys/users/' + name +
'/edit/')
if not user_link:
create_user(session_browser, name, 'secret123')
@when(
parsers.parse('I create a user named {name:w} with password {password:w}'))
def create_user(session_browser, name, password):
_create_user(session_browser, name, password)
@when(parsers.parse('I rename the user {old_name:w} to {new_name:w}'))
def rename_user(session_browser, old_name, new_name):
_rename_user(session_browser, old_name, new_name)
@when(parsers.parse('I delete the user {name:w}'))
def delete_user(session_browser, name):
_delete_user(session_browser, name)
@then(parsers.parse('{name:w} should be listed as a user'))
def new_user_is_listed(session_browser, name):
assert _is_user(session_browser, name)
@then(parsers.parse('{name:w} should not be listed as a user'))
def new_user_is_not_listed(session_browser, name):
assert not _is_user(session_browser, name)
@when('I change the language to <language>')
def change_language(session_browser, language):
_set_language(session_browser, _language_codes[language])
@then('Plinth language should be <language>')
def plinth_language_should_be(session_browser, language):
assert _check_language(session_browser, _language_codes[language])
def _create_user(browser, name, password):
functional.nav_to_module(browser, 'users')
with functional.wait_for_page_update(browser):
browser.find_link_by_href('/plinth/sys/users/create/').first.click()
browser.find_by_id('id_username').fill(name)
browser.find_by_id('id_password1').fill(password)
browser.find_by_id('id_password2').fill(password)
functional.submit(browser)
def _rename_user(browser, old_name, new_name):
functional.nav_to_module(browser, 'users')
with functional.wait_for_page_update(browser):
browser.find_link_by_href('/plinth/sys/users/' + old_name +
'/edit/').first.click()
browser.find_by_id('id_username').fill(new_name)
functional.submit(browser)
def _delete_user(browser, name):
functional.nav_to_module(browser, 'users')
delete_link = browser.find_link_by_href('/plinth/sys/users/' + name +
'/delete/')
if delete_link:
with functional.wait_for_page_update(browser):
delete_link.first.click()
functional.submit(browser)
def _is_user(browser, name):
functional.nav_to_module(browser, 'users')
edit_link = browser.find_link_by_href('/plinth/sys/users/' + name +
'/edit/')
return bool(edit_link)
def _set_language(browser, language_code):
username = functional.config['DEFAULT']['username']
functional.visit(browser, '/plinth/sys/users/{}/edit/'.format(username))
browser.find_by_xpath('//select[@id="id_language"]//option[@value="' +
language_code + '"]').first.click()
functional.submit(browser)
def _check_language(browser, language_code):
functional.nav_to_module(browser, 'config')
return browser.find_by_css('.app-titles').first.find_by_tag(
'h2').first.value == _config_page_title_language_map[language_code]

View File

@ -8,7 +8,7 @@
# TODO Scenario: Make user active
# TODO Scenario: Change user password
@system @essential @users_groups
@system @essential @users
Feature: Users and Groups
Manage users and groups.

View File

@ -0,0 +1,448 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Utilities for functional testing.
"""
import configparser
import logging
import os
import pathlib
import tempfile
import time
from contextlib import contextmanager
import requests
from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait
config = configparser.ConfigParser()
config.read(pathlib.Path(__file__).with_name('config.ini'))
config['DEFAULT']['url'] = os.environ.get('FREEDOMBOX_URL',
config['DEFAULT']['url'])
config['DEFAULT']['samba_port'] = os.environ.get(
'FREEDOMBOX_SAMBA_PORT', config['DEFAULT']['samba_port'])
logger = logging.getLogger(__name__)
base_url = config['DEFAULT']['url']
_app_checkbox_id = {
'tor': 'id_tor-enabled',
'openvpn': 'id_openvpn-enabled',
}
_apps_with_loaders = ['tor']
# unlisted sites just use '/' + site_name as url
_site_url = {
'wiki': '/ikiwiki',
'jsxc': '/plinth/apps/jsxc/jsxc/',
'cockpit': '/_cockpit/',
'syncthing': '/syncthing/',
}
_sys_modules = [
'avahi', 'backups', 'bind', 'cockpit', 'config', 'datetime', 'diagnostics',
'dynamicdns', 'firewall', 'letsencrypt', 'monkeysphere', 'names',
'networks', 'pagekite', 'performance', 'power', 'security', 'snapshot',
'ssh', 'storage', 'upgrades', 'users'
]
######################
# Browser Extensions #
######################
def visit(browser, path):
"""Visit a path assuming the base URL as configured."""
browser.visit(config['DEFAULT']['url'] + path)
def eventually(function, args=[], timeout=30):
"""Execute a function returning a boolean expression till it returns
True or a timeout is reached"""
end_time = time.time() + timeout
current_time = time.time()
while current_time < end_time:
if function(*args):
return True
time.sleep(0.1)
current_time = time.time()
return False
class _PageLoaded():
"""
Wait until a page (re)loaded.
- element: Wait until this element gets stale
- expected_url (optional): Wait for the URL to become <expected_url>.
This can be necessary to wait for a redirect to finish.
"""
def __init__(self, element, expected_url=None):
self.element = element
self.expected_url = expected_url
def __call__(self, driver):
is_stale = False
try:
self.element.has_class('whatever_class')
except StaleElementReferenceException:
if self.expected_url is None:
is_stale = True
else:
if driver.url.endswith(self.expected_url):
is_stale = True
return is_stale
@contextmanager
def wait_for_page_update(browser, timeout=300, expected_url=None):
page_body = browser.find_by_tag('body').first
yield
WebDriverWait(browser, timeout).until(_PageLoaded(page_body, expected_url))
def _get_site_url(site_name):
if site_name.startswith('share'):
site_name = site_name.replace('_', '/')
url = '/' + site_name
url = _site_url.get(site_name, url)
return url
def access_url(browser, site_name):
browser.visit(config['DEFAULT']['url'] + _get_site_url(site_name))
def is_available(browser, site_name):
url_to_visit = config['DEFAULT']['url'] + _get_site_url(site_name)
browser.visit(url_to_visit)
time.sleep(3)
browser.reload()
not_404 = '404' not in browser.title
# The site might have a default path after the sitename,
# e.g /mediawiki/Main_Page
no_redirect = browser.url.startswith(url_to_visit.strip('/'))
return not_404 and no_redirect
def download_file(browser, url):
"""Return file contents after downloading a URL."""
cookies = browser.cookies.all()
response = requests.get(url, cookies=cookies, verify=False)
if response.status_code != 200:
raise Exception('URL download failed')
return response.content
def download_file_outside_browser(url):
"""Download a file to disk given a URL."""
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
logging.captureWarnings(True)
request = requests.get(url, verify=False)
logging.captureWarnings(False)
temp_file.write(request.content)
return temp_file.name
###########################
# Form handling utilities #
###########################
def submit(browser, element=None, form_class=None, expected_url=None):
with wait_for_page_update(browser, expected_url=expected_url):
if element:
element.click()
elif form_class:
browser.find_by_css(
'.{} input[type=submit]'.format(form_class)).click()
else:
browser.find_by_css('input[type=submit]').click()
def change_checkbox_status(browser, app_name, checkbox_id,
change_status_to='enabled'):
"""Change checkbox status."""
checkbox = browser.find_by_id(checkbox_id)
if change_status_to == 'enabled':
checkbox.check()
else:
checkbox.uncheck()
submit(browser, form_class='form-configuration')
if app_name in _apps_with_loaders:
wait_for_config_update(browser, app_name)
def wait_for_config_update(browser, app_name):
while browser.is_element_present_by_css('.running-status.loading'):
time.sleep(0.1)
############################
# Login handling utilities #
############################
def is_login_prompt(browser):
return all(
[browser.find_by_id('id_username'),
browser.find_by_id('id_password')])
def _create_admin_account(browser, username, password):
browser.find_by_id('id_username').fill(username)
browser.find_by_id('id_password1').fill(password)
browser.find_by_id('id_password2').fill(password)
submit(browser)
def login(browser, url, username, password):
# XXX: Find a way to remove the hardcoded jsxc URL
if '/plinth/' not in browser.url or '/jsxc/jsxc' in browser.url:
browser.visit(url)
apps_link = browser.find_link_by_href('/plinth/apps/')
if len(apps_link):
return
login_button = browser.find_link_by_href('/plinth/accounts/login/')
if login_button:
login_button.first.click()
if login_button:
browser.fill('username', username)
browser.fill('password', password)
submit(browser)
else:
browser.visit(base_url + '/plinth/firstboot/welcome')
submit(browser) # click the "Start Setup" button
_create_admin_account(browser, username, password)
if '/network-topology-first-boot' in browser.url:
submit(browser, element=browser.find_by_name('skip')[0])
if '/internet-connection-type' in browser.url:
submit(browser, element=browser.find_by_name('skip')[0])
#################
# App utilities #
#################
def nav_to_module(browser, module):
sys_or_apps = 'sys' if module in _sys_modules else 'apps'
required_url = base_url + f'/plinth/{sys_or_apps}/{module}/'
if browser.url != required_url:
browser.visit(required_url)
def app_select_domain_name(browser, app_name, domain_name):
browser.visit('{}/plinth/apps/{}/setup/'.format(base_url, app_name))
drop_down = browser.find_by_id('id_domain_name')
drop_down.select(domain_name)
submit(browser, form_class='form-configuration')
#########################
# App install utilities #
#########################
def _find_install_button(browser, app_name):
nav_to_module(browser, app_name)
return browser.find_by_css('.form-install input[type=submit]')
def is_installed(browser, app_name):
install_button = _find_install_button(browser, app_name)
return not bool(install_button)
def install(browser, app_name):
install_button = _find_install_button(browser, app_name)
def install_in_progress():
selectors = [
'.install-state-' + state
for state in ['pre', 'post', 'installing']
]
return any(
browser.is_element_present_by_css(selector)
for selector in selectors)
def is_server_restarting():
return browser.is_element_present_by_css('.neterror')
def wait_for_install():
if install_in_progress():
time.sleep(1)
elif is_server_restarting():
time.sleep(1)
browser.visit(browser.url)
else:
return
wait_for_install()
if install_button:
install_button.click()
wait_for_install()
# sleep(2) # XXX This shouldn't be required.
################################
# App enable/disable utilities #
################################
def _change_app_status(browser, app_name, change_status_to='enabled'):
"""Enable or disable application."""
button = browser.find_by_css('button[name="app_enable_disable_button"]')
if button:
should_enable_field = browser.find_by_id('id_should_enable')
if (should_enable_field.value == 'False'
and change_status_to == 'disabled') or (
should_enable_field.value == 'True'
and change_status_to == 'enabled'):
submit(browser, element=button)
else:
checkbox_id = _app_checkbox_id[app_name]
change_checkbox_status(browser, app_name, checkbox_id,
change_status_to)
if app_name in _apps_with_loaders:
wait_for_config_update(browser, app_name)
def app_enable(browser, app_name):
nav_to_module(browser, app_name)
_change_app_status(browser, app_name, 'enabled')
def app_disable(browser, app_name):
nav_to_module(browser, app_name)
_change_app_status(browser, app_name, 'disabled')
def app_can_be_disabled(browser, app_name):
"""Return whether the application can be disabled."""
nav_to_module(browser, app_name)
button = browser.find_by_css('button[name="app_enable_disable_button"]')
return bool(button)
#########################
# Domain name utilities #
#########################
def set_domain_name(browser, domain_name):
nav_to_module(browser, 'config')
browser.find_by_id('id_domainname').fill(domain_name)
submit(browser)
########################
# Front page utilities #
########################
def find_on_front_page(browser, app_name):
browser.visit(base_url)
shortcuts = browser.find_link_by_href(f'/{app_name}/')
return shortcuts
####################
# Daemon utilities #
####################
def service_is_running(browser, app_name):
nav_to_module(browser, app_name)
return len(browser.find_by_id('service-not-running')) == 0
def service_is_not_running(browser, app_name):
nav_to_module(browser, app_name)
return len(browser.find_by_id('service-not-running')) != 0
##############################
# System -> Config utilities #
##############################
def set_advanced_mode(browser, mode):
nav_to_module(browser, 'config')
advanced_mode = browser.find_by_id('id_advanced_mode')
if mode:
advanced_mode.check()
else:
advanced_mode.uncheck()
submit(browser)
####################
# Backup utilities #
####################
def _click_button_and_confirm(browser, href):
buttons = browser.find_link_by_href(href)
if buttons:
buttons.first.click()
with wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
submit(browser)
def _backup_delete_archive_by_name(browser, archive_name):
nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/delete/{archive_name}/'
_click_button_and_confirm(browser, href)
def backup_create(browser, app_name, archive_name=None):
install(browser, 'backups')
if archive_name:
_backup_delete_archive_by_name(browser, archive_name)
browser.find_link_by_href('/plinth/sys/backups/create/').first.click()
browser.find_by_id('select-all').uncheck()
if archive_name:
browser.find_by_id('id_backups-name').fill(archive_name)
# ensure the checkbox is scrolled into view
browser.execute_script('window.scrollTo(0, 0)')
browser.find_by_value(app_name).first.check()
submit(browser)
def backup_restore(browser, app_name, archive_name=None):
nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/restore-archive/{archive_name}/'
_click_button_and_confirm(browser, href)
######################
# Networks utilities #
######################
def networks_set_firewall_zone(browser, zone):
""""Set the network device firewall zone as internal or external."""
nav_to_module(browser, 'networks')
device = browser.find_by_xpath(
'//span[contains(@class, "label-success") '
'and contains(@class, "connection-status-label")]/following::a').first
network_id = device['href'].split('/')[-3]
device.click()
edit_url = "/plinth/sys/networks/{}/edit/".format(network_id)
browser.find_link_by_href(edit_url).first.click()
browser.select('zone', zone)
browser.find_by_tag("form").first.find_by_tag('input')[-1].click()
##################
# Bind utilities #
##################
def set_forwarders(browser, forwarders):
"""Set the forwarders list (space separated) in bind configuration."""
nav_to_module(browser, 'bind')
browser.fill('forwarders', forwarders)
submit(browser, form_class='form-configuration')
def get_forwarders(browser):
"""Return the forwarders list (space separated) in bind configuration."""
nav_to_module(browser, 'bind')
return browser.find_by_name('forwarders').first.value

View File

@ -0,0 +1,163 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Step definitions used across apps.
"""
import time
import pytest
from pytest_bdd import given, parsers, then, when
from plinth.tests import functional
@given("I'm a logged in user")
def logged_in_user(session_browser):
functional.login(session_browser, functional.base_url,
functional.config['DEFAULT']['username'],
functional.config['DEFAULT']['password'])
@given("I'm a logged out user")
def logged_out_user(session_browser):
functional.visit(session_browser, '/plinth/accounts/logout/')
@when("I log out")
def log_out_user(session_browser):
functional.visit(session_browser, '/plinth/accounts/logout/')
@given(parsers.parse('the {app_name:w} application is installed'))
def application_is_installed(session_browser, app_name):
functional.install(session_browser, app_name)
assert (functional.is_installed(session_browser, app_name))
@given(parsers.parse('the {app_name:w} application is enabled'))
def application_is_enabled(session_browser, app_name):
functional.app_enable(session_browser, app_name)
@given(parsers.parse('the {app_name:w} application is disabled'))
def application_is_disabled(session_browser, app_name):
functional.app_disable(session_browser, app_name)
@when(parsers.parse('I enable the {app_name:w} application'))
def enable_application(session_browser, app_name):
functional.app_enable(session_browser, app_name)
@when(parsers.parse('I disable the {app_name:w} application'))
def disable_application(session_browser, app_name):
functional.app_disable(session_browser, app_name)
@given(parsers.parse('the {app_name:w} application can be disabled'))
def app_can_be_disabled(session_browser, app_name):
if not functional.app_can_be_disabled(session_browser, app_name):
pytest.skip(f'network time application can\'t be disabled')
@then(parsers.parse('the {service_name:w} service should be running'))
def service_should_be_running(session_browser, service_name):
assert functional.eventually(functional.service_is_running,
args=[session_browser, service_name])
@then(parsers.parse('the {service_name:w} service should not be running'))
def service_should_not_be_running(session_browser, service_name):
assert functional.eventually(functional.service_is_not_running,
args=[session_browser, service_name])
@then(parsers.parse('I should be prompted for login'))
def prompted_for_login(session_browser):
assert functional.is_login_prompt(session_browser)
@given(parsers.parse('the domain name is set to {domain:S}'))
def step_set_domain_name(session_browser, domain):
functional.set_domain_name(session_browser, domain)
@then(parsers.parse('the {site_name:w} site should be available'))
def site_should_be_available(session_browser, site_name):
assert functional.is_available(session_browser, site_name)
@then(parsers.parse('the {site_name:w} site should not be available'))
def site_should_not_be_available(session_browser, site_name):
assert not functional.is_available(session_browser, site_name)
@when(parsers.parse('I access {app_name:w} application'))
def access_application(session_browser, app_name):
functional.access_url(session_browser, app_name)
@given('advanced mode is on')
def advanced_mode_is_on(session_browser):
functional.set_advanced_mode(session_browser, True)
@when(
parsers.parse('I create a backup of the {app_name:w} app data with '
'name {archive_name:w}'))
def backup_create(session_browser, app_name, archive_name):
functional.backup_create(session_browser, app_name, archive_name)
@when(parsers.parse('I wait for {seconds} seconds'))
def sleep_for(seconds):
seconds = int(seconds)
time.sleep(seconds)
@when(
parsers.parse(
'I restore the {app_name:w} app data backup with name {archive_name:w}'
))
def backup_restore(session_browser, app_name, archive_name):
functional.backup_restore(session_browser, app_name, archive_name)
@given(parsers.parse('the network device is in the {zone:w} firewall zone'))
def networks_set_firewall_zone(session_browser, zone):
functional.networks_set_firewall_zone(session_browser, zone)
@given(
parsers.parse('the domain name for {app_name:w} is set to {domain_name:S}')
)
def select_domain_name(session_browser, app_name, domain_name):
functional.app_select_domain_name(session_browser, app_name, domain_name)
@then(parsers.parse('{app_name:w} app should be visible on the front page'))
def app_visible_on_front_page(session_browser, app_name):
shortcuts = functional.find_on_front_page(session_browser, app_name)
assert len(shortcuts) == 1
@then(parsers.parse('{app_name:w} app should not be visible on the front page')
)
def app_not_visible_on_front_page(session_browser, app_name):
shortcuts = functional.find_on_front_page(session_browser, app_name)
assert len(shortcuts) == 0
@given(parsers.parse('bind forwarders are set to {forwarders}'))
def bind_given_set_forwarders(session_browser, forwarders):
functional.set_forwarders(session_browser, forwarders)
@when(parsers.parse('I set bind forwarders to {forwarders}'))
def bind_set_forwarders(session_browser, forwarders):
functional.set_forwarders(session_browser, forwarders)
@then(parsers.parse('bind forwarders should be {forwarders}'))
def bind_assert_forwarders(session_browser, forwarders):
assert functional.get_forwarders(session_browser) == forwarders

View File

@ -1,639 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import pytest
import splinter
from pytest_bdd import given, parsers, then, when
from ..support import application
@given(parsers.parse('the {app_name:w} application is installed'))
def application_is_installed(session_browser, app_name):
application.install(session_browser, app_name)
assert (application.is_installed(session_browser, app_name))
@given(parsers.parse('the {app_name:w} application is enabled'))
def application_is_enabled(session_browser, app_name):
application.enable(session_browser, app_name)
@given(parsers.parse('the {app_name:w} application is disabled'))
def application_is_disabled(session_browser, app_name):
application.disable(session_browser, app_name)
@given(parsers.parse('the network time application is enabled'))
def ntp_is_enabled(session_browser):
application.enable(session_browser, 'ntp')
@given(parsers.parse('the network time application is disabled'))
def ntp_is_disabled(session_browser):
application.disable(session_browser, 'ntp')
@given(parsers.parse('the network time application can be disabled'))
def ntp_can_be_disabled(session_browser):
if not application.can_be_disabled(session_browser, 'ntp'):
pytest.skip(f'network time application can\'t be disabled')
@when(parsers.parse('I set the time zone to {time_zone:S}'))
def time_zone_set(session_browser, time_zone):
application.time_zone_set(session_browser, time_zone)
@then(parsers.parse('the time zone should be {time_zone:S}'))
def time_zone_assert(session_browser, time_zone):
assert time_zone == application.time_zone_get(session_browser)
@given(parsers.parse('the service discovery application is enabled'))
def avahi_is_enabled(session_browser):
application.enable(session_browser, 'avahi')
@given(parsers.parse('the service discovery application is disabled'))
def avahi_is_disabled(session_browser):
application.disable(session_browser, 'avahi')
@when(parsers.parse('I enable the {app_name:w} application'))
def enable_application(session_browser, app_name):
application.enable(session_browser, app_name)
@when(parsers.parse('I disable the {app_name:w} application'))
def disable_application(session_browser, app_name):
application.disable(session_browser, app_name)
@when(parsers.parse('I enable the network time application'))
def enable_ntp(session_browser):
application.enable(session_browser, 'ntp')
@when(parsers.parse('I disable the network time application'))
def disable_ntp(session_browser):
application.disable(session_browser, 'ntp')
@when(parsers.parse('I enable the service discovery application'))
def enable_avahi(session_browser):
application.enable(session_browser, 'avahi')
@when(parsers.parse('I disable the service discovery application'))
def disable_avahi(session_browser):
application.disable(session_browser, 'avahi')
@given(
parsers.parse('the domain name for {app_name:w} is set to {domain_name:S}')
)
def select_domain_name(session_browser, app_name, domain_name):
application.select_domain_name(session_browser, app_name, domain_name)
@given('the shadowsocks application is configured')
def configure_shadowsocks(session_browser):
application.configure_shadowsocks(session_browser, 'example.com',
'fakepassword')
@when(
parsers.parse('I configure shadowsocks with server {server:S} and '
'password {password:w}'))
def configure_shadowsocks_with_details(session_browser, server, password):
application.configure_shadowsocks(session_browser, server, password)
@then(
parsers.parse('shadowsocks should be configured with server {server:S} '
'and password {password:w}'))
def assert_shadowsocks_configuration(session_browser, server, password):
assert (
server,
password) == application.shadowsocks_get_configuration(session_browser)
@when(parsers.parse('I modify the maximum file size of coquelicot to {size:d}')
)
def modify_max_file_size(session_browser, size):
application.modify_max_file_size(session_browser, size)
@then(parsers.parse('the maximum file size of coquelicot should be {size:d}'))
def assert_max_file_size(session_browser, size):
assert application.get_max_file_size(session_browser) == size
@when(parsers.parse('I modify the coquelicot upload password to {password:w}'))
def modify_upload_password(session_browser, password):
application.modify_upload_password(session_browser, password)
@given(parsers.parse('share {name:w} is not available'))
def remove_share(session_browser, name):
application.remove_share(session_browser, name)
@when(parsers.parse('I add a share {name:w} from path {path} for {group:w}'))
def add_share(session_browser, name, path, group):
application.add_share(session_browser, name, path, group)
@when(
parsers.parse('I edit share {old_name:w} to {new_name:w} from path {path} '
'for {group:w}'))
def edit_share(session_browser, old_name, new_name, path, group):
application.edit_share(session_browser, old_name, new_name, path, group)
@when(parsers.parse('I remove share {name:w}'))
def remove_share2(session_browser, name):
application.remove_share(session_browser, name)
@when(parsers.parse('I edit share {name:w} to be public'))
def edit_share_public_access(session_browser, name):
application.make_share_public(session_browser, name)
@then(
parsers.parse(
'the share {name:w} should be listed from path {path} for {group:w}'))
def verify_share(session_browser, name, path, group):
application.verify_share(session_browser, name, path, group)
@then(parsers.parse('the share {name:w} should not be listed'))
def verify_invalid_share(session_browser, name):
with pytest.raises(splinter.exceptions.ElementDoesNotExist):
application.get_share(session_browser, name)
@then(parsers.parse('the share {name:w} should be accessible'))
def access_share(session_browser, name):
application.access_share(session_browser, name)
@then(parsers.parse('the share {name:w} should not exist'))
def verify_nonexistant_share(session_browser, name):
application.verify_nonexistant_share(session_browser, name)
@then(parsers.parse('the share {name:w} should not be accessible'))
def verify_inaccessible_share(session_browser, name):
application.verify_inaccessible_share(session_browser, name)
@when(parsers.parse('I enable mediawiki public registrations'))
def enable_mediawiki_public_registrations(session_browser):
application.enable_mediawiki_public_registrations(session_browser)
@when(parsers.parse('I disable mediawiki public registrations'))
def disable_mediawiki_public_registrations(session_browser):
application.disable_mediawiki_public_registrations(session_browser)
@when(parsers.parse('I enable mediawiki private mode'))
def enable_mediawiki_private_mode(session_browser):
application.enable_mediawiki_private_mode(session_browser)
@when(parsers.parse('I disable mediawiki private mode'))
def disable_mediawiki_private_mode(session_browser):
application.disable_mediawiki_private_mode(session_browser)
@when(parsers.parse('I set the mediawiki admin password to {password}'))
def set_mediawiki_admin_password(session_browser, password):
application.set_mediawiki_admin_password(session_browser, password)
@when(parsers.parse('I enable message archive management'))
def ejabberd_enable_archive_management(session_browser):
application.enable_ejabberd_message_archive_management(session_browser)
@when(parsers.parse('I disable message archive management'))
def ejabberd_disable_archive_management(session_browser):
application.disable_ejabberd_message_archive_management(session_browser)
@when('there is an ikiwiki wiki')
def ikiwiki_create_wiki_if_needed(session_browser):
application.ikiwiki_create_wiki_if_needed(session_browser)
@when('I delete the ikiwiki wiki')
def ikiwiki_delete_wiki(session_browser):
application.ikiwiki_delete_wiki(session_browser)
@then('the ikiwiki wiki should be restored')
def ikiwiki_should_exist(session_browser):
assert application.ikiwiki_wiki_exists(session_browser)
@given('I have added a contact to my roster')
def ejabberd_add_contact(session_browser):
application.ejabberd_add_contact(session_browser)
@when('I delete the contact from my roster')
def ejabberd_delete_contact(session_browser):
application.ejabberd_delete_contact(session_browser)
@then('I should have a contact on my roster')
def ejabberd_should_have_contact(session_browser):
assert application.ejabberd_has_contact(session_browser)
@given(parsers.parse('tor relay is {enabled:w}'))
def tor_given_relay_enable(session_browser, enabled):
application.tor_feature_enable(session_browser, 'relay', enabled)
@when(parsers.parse('I {enable:w} tor relay'))
def tor_relay_enable(session_browser, enable):
application.tor_feature_enable(session_browser, 'relay', enable)
@then(parsers.parse('tor relay should be {enabled:w}'))
def tor_assert_relay_enabled(session_browser, enabled):
application.tor_assert_feature_enabled(session_browser, 'relay', enabled)
@then(parsers.parse('tor {port_name:w} port should be displayed'))
def tor_assert_port_displayed(session_browser, port_name):
assert port_name in application.tor_get_relay_ports(session_browser)
@given(parsers.parse('tor bridge relay is {enabled:w}'))
def tor_given_bridge_relay_enable(session_browser, enabled):
application.tor_feature_enable(session_browser, 'bridge-relay', enabled)
@when(parsers.parse('I {enable:w} tor bridge relay'))
def tor_bridge_relay_enable(session_browser, enable):
application.tor_feature_enable(session_browser, 'bridge-relay', enable)
@then(parsers.parse('tor bridge relay should be {enabled:w}'))
def tor_assert_bridge_relay_enabled(session_browser, enabled):
application.tor_assert_feature_enabled(session_browser, 'bridge-relay',
enabled)
@given(parsers.parse('tor hidden services are {enabled:w}'))
def tor_given_hidden_services_enable(session_browser, enabled):
application.tor_feature_enable(session_browser, 'hidden-services', enabled)
@when(parsers.parse('I {enable:w} tor hidden services'))
def tor_hidden_services_enable(session_browser, enable):
application.tor_feature_enable(session_browser, 'hidden-services', enable)
@then(parsers.parse('tor hidden services should be {enabled:w}'))
def tor_assert_hidden_services_enabled(session_browser, enabled):
application.tor_assert_feature_enabled(session_browser, 'hidden-services',
enabled)
@then(parsers.parse('tor hidden services information should be displayed'))
def tor_assert_hidden_services(session_browser):
application.tor_assert_hidden_services(session_browser)
@given(parsers.parse('download software packages over tor is {enabled:w}'))
def tor_given_download_software_over_tor_enable(session_browser, enabled):
application.tor_feature_enable(session_browser, 'software', enabled)
@when(parsers.parse('I {enable:w} download software packages over tor'))
def tor_download_software_over_tor_enable(session_browser, enable):
application.tor_feature_enable(session_browser, 'software', enable)
@then(
parsers.parse('download software packages over tor should be {enabled:w}'))
def tor_assert_download_software_over_tor(session_browser, enabled):
application.tor_assert_feature_enabled(session_browser, 'software',
enabled)
@then(
parsers.parse(
'{domain:S} should be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_introducer(session_browser, domain, introducer_type):
assert application.tahoe_get_introducer(session_browser, domain,
introducer_type)
@then(
parsers.parse(
'{domain:S} should not be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_not_introducer(session_browser, domain, introducer_type):
assert not application.tahoe_get_introducer(session_browser, domain,
introducer_type)
@given(parsers.parse('{domain:S} is not a tahoe introducer'))
def tahoe_given_remove_introducer(session_browser, domain):
if application.tahoe_get_introducer(session_browser, domain, 'connected'):
application.tahoe_remove_introducer(session_browser, domain)
@when(parsers.parse('I add {domain:S} as a tahoe introducer'))
def tahoe_add_introducer(session_browser, domain):
application.tahoe_add_introducer(session_browser, domain)
@given(parsers.parse('{domain:S} is a tahoe introducer'))
def tahoe_given_add_introducer(session_browser, domain):
if not application.tahoe_get_introducer(session_browser, domain,
'connected'):
application.tahoe_add_introducer(session_browser, domain)
@when(parsers.parse('I remove {domain:S} as a tahoe introducer'))
def tahoe_remove_introducer(session_browser, domain):
application.tahoe_remove_introducer(session_browser, domain)
@given('the access rights are set to "only the owner can view or make changes"'
)
def radicale_given_owner_only(session_browser):
application.radicale_set_access_rights(session_browser, 'owner_only')
@given('the access rights are set to "any user can view, but only the '
'owner can make changes"')
def radicale_given_owner_write(session_browser):
application.radicale_set_access_rights(session_browser, 'owner_write')
@given('the access rights are set to "any user can view or make changes"')
def radicale_given_authenticated(session_browser):
application.radicale_set_access_rights(session_browser, 'authenticated')
@when('I change the access rights to "only the owner can view or make changes"'
)
def radicale_set_owner_only(session_browser):
application.radicale_set_access_rights(session_browser, 'owner_only')
@when('I change the access rights to "any user can view, but only the '
'owner can make changes"')
def radicale_set_owner_write(session_browser):
application.radicale_set_access_rights(session_browser, 'owner_write')
@when('I change the access rights to "any user can view or make changes"')
def radicale_set_authenticated(session_browser):
application.radicale_set_access_rights(session_browser, 'authenticated')
@then('the access rights should be "only the owner can view or make changes"')
def radicale_check_owner_only(session_browser):
assert application.radicale_get_access_rights(
session_browser) == 'owner_only'
@then('the access rights should be "any user can view, but only the '
'owner can make changes"')
def radicale_check_owner_write(session_browser):
assert application.radicale_get_access_rights(
session_browser) == 'owner_write'
@then('the access rights should be "any user can view or make changes"')
def radicale_check_authenticated(session_browser):
assert application.radicale_get_access_rights(
session_browser) == 'authenticated'
@given(parsers.parse('the openvpn application is setup'))
def openvpn_setup(session_browser):
application.openvpn_setup(session_browser)
@given('I download openvpn profile')
def openvpn_download_profile(session_browser):
return application.openvpn_download_profile(session_browser)
@then('the openvpn profile should be downloadable')
def openvpn_profile_downloadable(session_browser):
application.openvpn_download_profile(session_browser)
@then('the openvpn profile downloaded should be same as before')
def openvpn_profile_download_compare(session_browser,
openvpn_download_profile):
new_profile = application.openvpn_download_profile(session_browser)
assert openvpn_download_profile == new_profile
@given('public access is enabled in searx')
def searx_public_access_enabled(session_browser):
application.searx_enable_public_access(session_browser)
@when('I enable public access in searx')
def searx_enable_public_access(session_browser):
application.searx_enable_public_access(session_browser)
@when('I disable public access in searx')
def searx_disable_public_access(session_browser):
application.searx_disable_public_access(session_browser)
@then(parsers.parse('{app_name:w} app should be visible on the front page'))
def app_visible_on_front_page(session_browser, app_name):
shortcuts = application.find_on_front_page(session_browser, app_name)
assert len(shortcuts) == 1
@then(parsers.parse('{app_name:w} app should not be visible on the front page')
)
def app_not_visible_on_front_page(session_browser, app_name):
shortcuts = application.find_on_front_page(session_browser, app_name)
assert len(shortcuts) == 0
@given('a public repository')
@given('a repository')
@given('at least one repository exists')
def gitweb_repo(session_browser):
application.gitweb_create_repo(session_browser, 'Test-repo', 'public',
True)
@given('a private repository')
def gitweb_private_repo(session_browser):
application.gitweb_create_repo(session_browser, 'Test-repo', 'private',
True)
@given('both public and private repositories exist')
def gitweb_public_and_private_repo(session_browser):
application.gitweb_create_repo(session_browser, 'Test-repo', 'public',
True)
application.gitweb_create_repo(session_browser, 'Test-repo2', 'private',
True)
@given(parsers.parse("a {access:w} repository that doesn't exist"))
def gitweb_nonexistent_repo(session_browser, access):
application.gitweb_delete_repo(session_browser, 'Test-repo',
ignore_missing=True)
return dict(access=access)
@given('all repositories are private')
def gitweb_all_repositories_private(session_browser):
application.gitweb_set_all_repos_private(session_browser)
@given(parsers.parse('a repository metadata:\n{metadata}'))
def gitweb_repo_metadata(session_browser, metadata):
metadata_dict = {}
for item in metadata.split('\n'):
item = item.split(': ')
metadata_dict[item[0]] = item[1]
return metadata_dict
@when('I create the repository')
def gitweb_create_repo(session_browser, access):
application.gitweb_create_repo(session_browser, 'Test-repo', access)
@when('I delete the repository')
def gitweb_delete_repo(session_browser):
application.gitweb_delete_repo(session_browser, 'Test-repo')
@when('I set the metadata of the repository')
def gitweb_edit_repo_metadata(session_browser, gitweb_repo_metadata):
application.gitweb_edit_repo_metadata(session_browser, 'Test-repo',
gitweb_repo_metadata)
@when('using a git client')
def gitweb_using_git_client():
pass
@then('the repository should be restored')
@then('the repository should be listed as a public')
def gitweb_repo_should_exists(session_browser):
assert application.gitweb_repo_exists(session_browser, 'Test-repo',
access='public')
@then('the repository should be listed as a private')
def gitweb_private_repo_should_exists(session_browser):
assert application.gitweb_repo_exists(session_browser, 'Test-repo',
'private')
@then('the repository should not be listed')
def gitweb_repo_should_not_exist(session_browser, gitweb_repo):
assert not application.gitweb_repo_exists(session_browser, gitweb_repo)
@then('the public repository should be listed on gitweb')
@then('the repository should be listed on gitweb')
def gitweb_repo_should_exist_on_gitweb(session_browser):
assert application.gitweb_site_repo_exists(session_browser, 'Test-repo')
@then('the private repository should not be listed on gitweb')
def gitweb_private_repo_should_exists_on_gitweb(session_browser):
assert not application.gitweb_site_repo_exists(session_browser,
'Test-repo2')
@then('the metadata of the repository should be as set')
def gitweb_repo_metadata_should_match(session_browser, gitweb_repo_metadata):
actual_metadata = application.gitweb_get_repo_metadata(
session_browser, 'Test-repo')
assert all(item in actual_metadata.items()
for item in gitweb_repo_metadata.items())
@then('the repository should be publicly readable')
def gitweb_repo_publicly_readable():
assert application.gitweb_repo_is_readable('Test-repo')
assert application.gitweb_repo_is_readable('Test-repo',
url_git_extension=True)
@then('the repository should not be publicly readable')
def gitweb_repo_not_publicly_readable():
assert not application.gitweb_repo_is_readable('Test-repo')
assert not application.gitweb_repo_is_readable('Test-repo',
url_git_extension=True)
@then('the repository should not be publicly writable')
def gitweb_repo_not_publicly_writable():
assert not application.gitweb_repo_is_writable('Test-repo')
assert not application.gitweb_repo_is_writable('Test-repo',
url_git_extension=True)
@then('the repository should be privately readable')
def gitweb_repo_privately_readable():
assert application.gitweb_repo_is_readable('Test-repo', with_auth=True)
assert application.gitweb_repo_is_readable('Test-repo', with_auth=True,
url_git_extension=True)
@then('the repository should be privately writable')
def gitweb_repo_privately_writable():
assert application.gitweb_repo_is_writable('Test-repo', with_auth=True)
assert application.gitweb_repo_is_writable('Test-repo', with_auth=True,
url_git_extension=True)
@when(parsers.parse('I {task:w} the {share_type:w} samba share'))
def samba_enable_share(session_browser, task, share_type):
if task == 'enable':
application.samba_set_share(session_browser, share_type,
status='enabled')
elif task == 'disable':
application.samba_set_share(session_browser, share_type,
status='disabled')
@then(parsers.parse('I can write to the {share_type:w} samba share'))
def samba_share_should_be_writable(share_type):
application.samba_assert_share_is_writable(share_type)
@then(parsers.parse('a guest user can write to the {share_type:w} samba share')
)
def samba_share_should_be_writable_to_guest(share_type):
application.samba_assert_share_is_writable(share_type, as_guest=True)
@then(
parsers.parse('a guest user can\'t access the {share_type:w} samba share'))
def samba_share_should_not_be_accessible_to_guest(share_type):
application.samba_assert_share_is_not_accessible(share_type, as_guest=True)
@then(parsers.parse('the {share_type:w} samba share should not be available'))
def samba_share_should_not_be_available(share_type):
application.samba_assert_share_is_not_available(share_type)

View File

@ -1,90 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pytest_bdd import given, parsers, then, when
from ..support import config, interface
default_url = config['DEFAULT']['url']
@given("I'm a logged in user")
def logged_in_user(session_browser):
interface.login(session_browser, default_url,
config['DEFAULT']['username'],
config['DEFAULT']['password'])
@given("I'm a logged out user")
def logged_out_user(session_browser):
session_browser.visit(default_url + '/plinth/accounts/logout/')
@when("I log out")
def log_out_user(session_browser):
session_browser.visit(default_url + '/plinth/accounts/logout/')
@then(parsers.parse('I should be prompted for login'))
def prompted_for_login(session_browser):
assert interface.is_login_prompt(session_browser)
@given(parsers.parse("the user {name:w} doesn't exist"))
def new_user_does_not_exist(session_browser, name):
interface.delete_user(session_browser, name)
@given(parsers.parse('the user {name:w} exists'))
def test_user_exists(session_browser, name):
interface.nav_to_module(session_browser, 'users')
user_link = session_browser.find_link_by_href('/plinth/sys/users/' + name +
'/edit/')
if not user_link:
create_user(session_browser, name, 'secret123')
@when(
parsers.parse('I create a user named {name:w} with password {password:w}'))
def create_user(session_browser, name, password):
interface.create_user(session_browser, name, password)
@when(parsers.parse('I rename the user {old_name:w} to {new_name:w}'))
def rename_user(session_browser, old_name, new_name):
interface.rename_user(session_browser, old_name, new_name)
@when(parsers.parse('I delete the user {name:w}'))
def delete_user(session_browser, name):
interface.delete_user(session_browser, name)
@then(parsers.parse('{name:w} should be listed as a user'))
def new_user_is_listed(session_browser, name):
assert interface.is_user(session_browser, name)
@then(parsers.parse('{name:w} should not be listed as a user'))
def new_user_is_not_listed(session_browser, name):
assert not interface.is_user(session_browser, name)
@given('a sample local file')
def sample_local_file():
file_path, contents = interface.create_sample_local_file()
return dict(file_path=file_path, contents=contents)
@when('I go to the status logs page')
def help_go_to_status_logs(session_browser):
interface.go_to_status_logs(session_browser)
@then('status logs should be shown')
def help_status_logs_are_shown(session_browser):
assert interface.are_status_logs_shown(session_browser)
@given(parsers.parse("I'm on the {name:w} page"))
def go_to_module(session_browser, name):
interface.nav_to_module(session_browser, name)

View File

@ -1,37 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pytest_bdd import parsers, then
from ..support import service
from ..support.service import eventually
@then(parsers.parse('the {service_name:w} service should be running'))
def service_should_be_running(session_browser, service_name):
assert eventually(service.is_running, args=[session_browser, service_name])
@then(parsers.parse('the {service_name:w} service should not be running'))
def service_should_not_be_running(session_browser, service_name):
assert eventually(service.is_not_running,
args=[session_browser, service_name])
@then(parsers.parse('the network time service should be running'))
def ntp_should_be_running(session_browser):
assert service.is_running(session_browser, 'ntp')
@then(parsers.parse('the network time service should not be running'))
def ntp_should_not_be_running(session_browser):
assert not service.is_running(session_browser, 'ntp')
@then(parsers.parse('the service discovery service should be running'))
def avahi_should_be_running(session_browser):
assert service.is_running(session_browser, 'avahi')
@then(parsers.parse('the service discovery service should not be running'))
def avahi_should_not_be_running(session_browser):
assert not service.is_running(session_browser, 'avahi')

View File

@ -1,234 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pytest_bdd import given, parsers, then, when
from ..support import interface, site
@then(parsers.parse('the {site_name:w} site should be available'))
def site_should_be_available(session_browser, site_name):
assert site.is_available(session_browser, site_name)
@then(parsers.parse('the {site_name:w} site should not be available'))
def site_should_not_be_available(session_browser, site_name):
assert not site.is_available(session_browser, site_name)
@when(parsers.parse('I access {app_name:w} application'))
def access_application(session_browser, app_name):
site.access_url(session_browser, app_name)
@when(
parsers.parse(
'I upload an image named {image:S} to mediawiki with credentials '
'{username:w} and {password:w}'))
def upload_image(session_browser, username, password, image):
site.upload_image_mediawiki(session_browser, username, password, image)
@then(parsers.parse('there should be {image:S} image'))
def uploaded_image_should_be_available(session_browser, image):
uploaded_image = site.get_uploaded_image_in_mediawiki(
session_browser, image)
assert image.lower() == uploaded_image.lower()
@then(
parsers.parse(
'I should be able to login to coquelicot with password {password:w}'))
def verify_upload_password(session_browser, password):
site.verify_coquelicot_upload_password(session_browser, password)
@when(
parsers.parse('I upload the sample local file to coquelicot with password '
'{password:w}'))
def coquelicot_upload_file(session_browser, sample_local_file, password):
url = site.upload_file_to_coquelicot(session_browser,
sample_local_file['file_path'],
password)
sample_local_file['upload_url'] = url
@when('I download the uploaded file from coquelicot')
def coquelicot_download_file(sample_local_file):
file_path = interface.download_file(sample_local_file['upload_url'])
sample_local_file['download_path'] = file_path
@then('contents of downloaded sample file should be same as sample local file')
def coquelicot_compare_upload_download_files(sample_local_file):
interface.compare_files(sample_local_file['file_path'],
sample_local_file['download_path'])
@then(parsers.parse('the mediawiki site should allow creating accounts'))
def mediawiki_allows_creating_accounts(session_browser):
site.verify_mediawiki_create_account_link(session_browser)
@then(parsers.parse('the mediawiki site should not allow creating accounts'))
def mediawiki_does_not_allow_creating_accounts(session_browser):
site.verify_mediawiki_no_create_account_link(session_browser)
@then(
parsers.parse('the mediawiki site should allow anonymous reads and writes')
)
def mediawiki_allows_anonymous_reads_edits(session_browser):
site.verify_mediawiki_anonymous_reads_edits_link(session_browser)
@then(
parsers.parse(
'the mediawiki site should not allow anonymous reads and writes'))
def mediawiki_does_not_allow__account_creation_anonymous_reads_edits(
session_browser):
site.verify_mediawiki_no_anonymous_reads_edits_link(session_browser)
@then(
parsers.parse(
'I should see the Upload File option in the side pane when logged in '
'with credentials {username:w} and {password:w}'))
def login_to_mediawiki_with_credentials(session_browser, username, password):
site.login_to_mediawiki_with_credentials(session_browser, username,
password)
@when('I delete the mediawiki main page')
def mediawiki_delete_main_page(session_browser):
site.mediawiki_delete_main_page(session_browser)
@then('the mediawiki main page should be restored')
def mediawiki_verify_text(session_browser):
assert site.mediawiki_has_main_page(session_browser)
@when('all ed2k files are removed from mldonkey')
def mldonkey_remove_all_ed2k_files(session_browser):
site.mldonkey_remove_all_ed2k_files(session_browser)
@when('I upload a sample ed2k file to mldonkey')
def mldonkey_upload_sample_ed2k_file(session_browser):
site.mldonkey_upload_sample_ed2k_file(session_browser)
@then(
parsers.parse(
'there should be {ed2k_files_number:d} ed2k files listed in mldonkey'))
def mldonkey_assert_number_of_ed2k_files(session_browser, ed2k_files_number):
assert ed2k_files_number == site.mldonkey_get_number_of_ed2k_files(
session_browser)
@when('all torrents are removed from transmission')
def transmission_remove_all_torrents(session_browser):
site.transmission_remove_all_torrents(session_browser)
@when('I upload a sample torrent to transmission')
def transmission_upload_sample_torrent(session_browser):
site.transmission_upload_sample_torrent(session_browser)
@then(
parsers.parse(
'there should be {torrents_number:d} torrents listed in transmission'))
def transmission_assert_number_of_torrents(session_browser, torrents_number):
assert torrents_number == site.transmission_get_number_of_torrents(
session_browser)
@when('all torrents are removed from deluge')
def deluge_remove_all_torrents(session_browser):
site.deluge_remove_all_torrents(session_browser)
@when('I upload a sample torrent to deluge')
def deluge_upload_sample_torrent(session_browser):
site.deluge_upload_sample_torrent(session_browser)
@then(
parsers.parse(
'there should be {torrents_number:d} torrents listed in deluge'))
def deluge_assert_number_of_torrents(session_browser, torrents_number):
assert torrents_number == site.deluge_get_number_of_torrents(
session_browser)
@then('the calendar should be available')
def assert_calendar_is_available(session_browser):
assert site.calendar_is_available(session_browser)
@then('the calendar should not be available')
def assert_calendar_is_not_available(session_browser):
assert not site.calendar_is_available(session_browser)
@then('the addressbook should be available')
def assert_addressbook_is_available(session_browser):
assert site.addressbook_is_available(session_browser)
@then('the addressbook should not be available')
def assert_addressbook_is_not_available(session_browser):
assert not site.addressbook_is_available(session_browser)
@given(parsers.parse('syncthing folder {folder_name:w} is not present'))
def syncthing_folder_not_present(session_browser, folder_name):
if site.syncthing_folder_is_present(session_browser, folder_name):
site.syncthing_remove_folder(session_browser, folder_name)
@given(
parsers.parse(
'folder {folder_path:S} is present as syncthing folder {folder_name:w}'
))
def syncthing_folder_present(session_browser, folder_name, folder_path):
if not site.syncthing_folder_is_present(session_browser, folder_name):
site.syncthing_add_folder(session_browser, folder_name, folder_path)
@when(
parsers.parse(
'I add a folder {folder_path:S} as syncthing folder {folder_name:w}'))
def syncthing_add_folder(session_browser, folder_name, folder_path):
site.syncthing_add_folder(session_browser, folder_name, folder_path)
@when(parsers.parse('I remove syncthing folder {folder_name:w}'))
def syncthing_remove_folder(session_browser, folder_name):
site.syncthing_remove_folder(session_browser, folder_name)
@then(parsers.parse('syncthing folder {folder_name:w} should be present'))
def syncthing_assert_folder_present(session_browser, folder_name):
assert site.syncthing_folder_is_present(session_browser, folder_name)
@then(parsers.parse('syncthing folder {folder_name:w} should not be present'))
def syncthing_assert_folder_not_present(session_browser, folder_name):
assert not site.syncthing_folder_is_present(session_browser, folder_name)
@given('I subscribe to a feed in ttrss')
def ttrss_subscribe(session_browser):
site.ttrss_subscribe(session_browser)
@when('I unsubscribe from the feed in ttrss')
def ttrss_unsubscribe(session_browser):
site.ttrss_unsubscribe(session_browser)
@then('I should be subscribed to the feed in ttrss')
def ttrss_assert_subscribed(session_browser):
assert site.ttrss_is_subscribed(session_browser)

View File

@ -1,341 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
import time
from pytest import fixture
from pytest_bdd import given, parsers, then, when
from ..support import system
language_codes = {
'Deutsch': 'de',
'Nederlands': 'nl',
'Português': 'pt',
'Türkçe': 'tr',
'dansk': 'da',
'español': 'es',
'français': 'fr',
'norsk (bokmål)': 'nb',
'polski': 'pl',
'svenska': 'sv',
'Русский': 'ru',
'తెలుగు': 'te',
'简体中文': 'zh-hans'
}
@fixture(scope='session')
def downloaded_file_info():
return dict()
@given(parsers.parse('the home page is {app_name:w}'))
def set_home_page(session_browser, app_name):
system.set_home_page(session_browser, app_name)
@given(parsers.parse('the domain name is set to {domain:S}'))
def set_domain_name(session_browser, domain):
system.set_domain_name(session_browser, domain)
@given('advanced mode is on')
def advanced_mode_is_on(session_browser):
system.set_advanced_mode(session_browser, True)
@when(parsers.parse('I change the hostname to {hostname:w}'))
def change_hostname_to(session_browser, hostname):
system.set_hostname(session_browser, hostname)
@when(parsers.parse('I change the domain name to {domain:S}'))
def change_domain_name_to(session_browser, domain):
system.set_domain_name(session_browser, domain)
@when(parsers.parse('I change the home page to {app_name:w}'))
def change_home_page_to(session_browser, app_name):
system.set_home_page(session_browser, app_name)
@when('I change the language to <language>')
def change_language(session_browser, language):
system.set_language(session_browser, language_codes[language])
@then(parsers.parse('the hostname should be {hostname:w}'))
def hostname_should_be(session_browser, hostname):
assert system.get_hostname(session_browser) == hostname
@then(parsers.parse('the domain name should be {domain:S}'))
def domain_name_should_be(session_browser, domain):
assert system.get_domain_name(session_browser) == domain
@then('Plinth language should be <language>')
def plinth_language_should_be(session_browser, language):
assert system.check_language(session_browser, language_codes[language])
@given('the list of snapshots is empty')
def empty_snapshots_list(session_browser):
system.delete_all_snapshots(session_browser)
@when('I manually create a snapshot')
def create_snapshot(session_browser):
system.create_snapshot(session_browser)
@then(parsers.parse('there should be {count:d} snapshot in the list'))
def verify_snapshot_count(session_browser, count):
num_snapshots = system.get_snapshot_count(session_browser)
assert num_snapshots == count
@given(
parsers.parse(
'snapshots are configured with free space {free_space:d}, timeline '
'snapshots {timeline_enabled:w}, software snapshots '
'{software_enabled:w}, hourly limit {hourly:d}, daily limit {daily:d}'
', weekly limit {weekly:d}, monthly limit {monthly:d}, yearly limit '
'{yearly:d}'))
def snapshot_given_set_configuration(session_browser, free_space,
timeline_enabled, software_enabled,
hourly, daily, weekly, monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
system.snapshot_set_configuration(session_browser, free_space,
timeline_enabled, software_enabled,
hourly, daily, weekly, monthly, yearly)
@when(
parsers.parse(
'I configure snapshots with free space {free_space:d}, '
'timeline snapshots {timeline_enabled:w}, '
'software snapshots {software_enabled:w}, hourly limit {hourly:d}, '
'daily limit {daily:d}, weekly limit {weekly:d}, monthly limit '
'{monthly:d}, yearly limit {yearly:d}'))
def snapshot_set_configuration(session_browser, free_space, timeline_enabled,
software_enabled, hourly, daily, weekly,
monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
system.snapshot_set_configuration(session_browser, free_space,
timeline_enabled, software_enabled,
hourly, daily, weekly, monthly, yearly)
@then(
parsers.parse(
'snapshots should be configured with free space {free_space:d}, '
'timeline snapshots {timeline_enabled:w}, software snapshots '
'{software_enabled:w}, hourly limit {hourly:d}, daily limit '
'{daily:d}, weekly limit {weekly:d}, monthly limit {monthly:d}, '
'yearly limit {yearly:d}'))
def snapshot_assert_configuration(session_browser, free_space,
timeline_enabled, software_enabled, hourly,
daily, weekly, monthly, yearly):
timeline_enabled = (timeline_enabled == 'enabled')
software_enabled = (software_enabled == 'enabled')
assert (free_space, timeline_enabled, software_enabled, hourly, daily,
weekly, monthly,
yearly) == system.snapshot_get_configuration(session_browser)
@then(parsers.parse('the home page should be {app_name:w}'))
def home_page_should_be(session_browser, app_name):
assert system.check_home_page_redirect(session_browser, app_name)
@given('dynamicdns is configured')
def dynamicdns_configure(session_browser):
system.dynamicdns_configure(session_browser)
@when('I change the dynamicdns configuration')
def dynamicdns_change_config(session_browser):
system.dynamicdns_change_config(session_browser)
@then('dynamicdns should have the original configuration')
def dynamicdns_has_original_config(session_browser):
assert system.dynamicdns_has_original_config(session_browser)
@when(
parsers.parse('I create a backup of the {app_name:w} app data with '
'name {archive_name:w}'))
def backup_create(session_browser, app_name, archive_name):
system.backup_create(session_browser, app_name, archive_name)
@when(
parsers.parse('I download the app data backup with name {archive_name:w}'))
def backup_download(session_browser, downloaded_file_info, archive_name):
file_path = system.download_backup(session_browser, archive_name)
downloaded_file_info['path'] = file_path
@when(
parsers.parse(
'I restore the {app_name:w} app data backup with name {archive_name:w}'
))
def backup_restore(session_browser, app_name, archive_name):
system.backup_restore(session_browser, app_name, archive_name)
@when(parsers.parse('I restore the downloaded app data backup'))
def backup_restore_from_upload(session_browser, app_name,
downloaded_file_info):
path = downloaded_file_info["path"]
try:
system.backup_upload_and_restore(session_browser, app_name, path)
except Exception as err:
raise err
finally:
os.remove(path)
@when(
parsers.parse('I configure pagekite with host {host:S}, port {port:d}, '
'kite name {kite_name:S} and kite secret {kite_secret:w}'))
def pagekite_configure(session_browser, host, port, kite_name, kite_secret):
system.pagekite_configure(session_browser, host, port, kite_name,
kite_secret)
@then(
parsers.parse(
'pagekite should be configured with host {host:S}, port {port:d}, '
'kite name {kite_name:S} and kite secret {kite_secret:w}'))
def pagekite_assert_configured(session_browser, host, port, kite_name,
kite_secret):
assert (host, port, kite_name,
kite_secret) == system.pagekite_get_configuration(session_browser)
@given(parsers.parse('bind forwarders are set to {forwarders}'))
def bind_given_set_forwarders(session_browser, forwarders):
system.bind_set_forwarders(session_browser, forwarders)
@when(parsers.parse('I set bind forwarders to {forwarders}'))
def bind_set_forwarders(session_browser, forwarders):
system.bind_set_forwarders(session_browser, forwarders)
@then(parsers.parse('bind forwarders should be {forwarders}'))
def bind_assert_forwarders(session_browser, forwarders):
assert system.bind_get_forwarders(session_browser) == forwarders
@given(parsers.parse('bind DNSSEC is {enable:w}'))
def bind_given_enable_dnssec(session_browser, enable):
should_enable = (enable == 'enabled')
system.bind_enable_dnssec(session_browser, should_enable)
@when(parsers.parse('I {enable:w} bind DNSSEC'))
def bind_enable_dnssec(session_browser, enable):
should_enable = (enable == 'enable')
system.bind_enable_dnssec(session_browser, should_enable)
@then(parsers.parse('bind DNSSEC should be {enabled:w}'))
def bind_assert_dnssec(session_browser, enabled):
assert system.bind_get_dnssec(session_browser) == (enabled == 'enabled')
@given(parsers.parse('restricted console logins are {enabled}'))
def security_given_enable_restricted_logins(session_browser, enabled):
should_enable = (enabled == 'enabled')
system.security_enable_restricted_logins(session_browser, should_enable)
@when(parsers.parse('I {enable} restricted console logins'))
def security_enable_restricted_logins(session_browser, enable):
should_enable = (enable == 'enable')
system.security_enable_restricted_logins(session_browser, should_enable)
@then(parsers.parse('restricted console logins should be {enabled}'))
def security_assert_restricted_logins(session_browser, enabled):
enabled = (enabled == 'enabled')
assert system.security_get_restricted_logins(session_browser) == enabled
@given(parsers.parse('automatic upgrades are {enabled:w}'))
def upgrades_given_enable_automatic(session_browser, enabled):
should_enable = (enabled == 'enabled')
system.upgrades_enable_automatic(session_browser, should_enable)
@when(parsers.parse('I {enable:w} automatic upgrades'))
def upgrades_enable_automatic(session_browser, enable):
should_enable = (enable == 'enable')
system.upgrades_enable_automatic(session_browser, should_enable)
@then(parsers.parse('automatic upgrades should be {enabled:w}'))
def upgrades_assert_automatic(session_browser, enabled):
should_be_enabled = (enabled == 'enabled')
assert system.upgrades_get_automatic(session_browser) == should_be_enabled
@given(
parsers.parse(
'the {key_type:w} key for {domain:S} is imported in monkeysphere'))
def monkeysphere_given_import_key(session_browser, key_type, domain):
system.monkeysphere_import_key(session_browser, key_type.lower(), domain)
@when(parsers.parse('I import {key_type:w} key for {domain:S} in monkeysphere')
)
def monkeysphere_import_key(session_browser, key_type, domain):
system.monkeysphere_import_key(session_browser, key_type.lower(), domain)
@then(
parsers.parse(
'the {key_type:w} key should imported for {domain:S} in monkeysphere'))
def monkeysphere_assert_imported_key(session_browser, key_type, domain):
system.monkeysphere_assert_imported_key(session_browser, key_type.lower(),
domain)
@then(
parsers.parse('I should be able to publish {key_type:w} key for '
'{domain:S} in monkeysphere'))
def monkeysphere_publish_key(session_browser, key_type, domain):
system.monkeysphere_publish_key(session_browser, key_type.lower(), domain)
@when(parsers.parse('I wait for {seconds} seconds'))
def sleep_for(seconds):
seconds = int(seconds)
time.sleep(seconds)
@when(parsers.parse('I open the main page'))
def open_main_page(session_browser):
system.open_main_page(session_browser)
@then(parsers.parse('the main page should be shown'))
def main_page_is_shown(session_browser):
assert (session_browser.url.endswith('/plinth/'))
@given(parsers.parse('the network device is in the {zone:w} firewall zone'))
def networks_set_firewall_zone(session_browser, zone):
system.networks_set_firewall_zone(session_browser, zone)
@then('the root disk should be shown')
def storage_root_disk_is_shown(session_browser):
assert system.storage_is_root_disk_shown(session_browser)

View File

@ -1,13 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import configparser
import os
import pathlib
config = configparser.ConfigParser()
config.read(pathlib.Path(__file__).parent.with_name('config.ini'))
config['DEFAULT']['url'] = os.environ.get('FREEDOMBOX_URL',
config['DEFAULT']['url'])
config['DEFAULT']['samba_port'] = os.environ.get(
'FREEDOMBOX_SAMBA_PORT', config['DEFAULT']['samba_port'])

View File

@ -1,752 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import contextlib
import os
import random
import shutil
import string
import subprocess
import tempfile
import urllib
from time import sleep
import requests
import splinter
from . import config, interface, site
from .interface import submit
from .service import eventually, wait_for_page_update
# unlisted apps just use the app_name as module name
app_module = {
'ntp': 'datetime',
'wiki': 'ikiwiki',
'tt-rss': 'ttrss',
}
app_checkbox_id = {
'tor': 'id_tor-enabled',
}
default_url = config['DEFAULT']['url']
apps_with_loaders = ['tor']
def get_app_module(app_name):
module = app_name
if app_name in app_module:
module = app_module[app_name]
return module
def _find_install_button(browser, app_name):
interface.nav_to_module(browser, get_app_module(app_name))
return browser.find_by_css('.form-install input[type=submit]')
def install(browser, app_name):
install_button = _find_install_button(browser, app_name)
def install_in_progress():
selectors = [
'.install-state-' + state
for state in ['pre', 'post', 'installing']
]
return any(
browser.is_element_present_by_css(selector)
for selector in selectors)
def is_server_restarting():
return browser.is_element_present_by_css('.neterror')
def wait_for_install():
if install_in_progress():
sleep(1)
elif is_server_restarting():
sleep(1)
browser.visit(browser.url)
else:
return
wait_for_install()
if install_button:
install_button.click()
wait_for_install()
# sleep(2) # XXX This shouldn't be required.
def is_installed(browser, app_name):
install_button = _find_install_button(browser, app_name)
return not bool(install_button)
def _change_app_status(browser, app_name, change_status_to='enabled'):
"""Enable or disable application."""
button = browser.find_by_css('button[name="app_enable_disable_button"]')
if button:
should_enable_field = browser.find_by_id('id_should_enable')
if (should_enable_field.value == 'False'
and change_status_to == 'disabled') or (
should_enable_field.value == 'True'
and change_status_to == 'enabled'):
interface.submit(browser, element=button)
else:
checkbox_id = app_checkbox_id[app_name]
_change_status(browser, app_name, checkbox_id, change_status_to)
if app_name in apps_with_loaders:
wait_for_config_update(browser, app_name)
def _change_status(browser, app_name, checkbox_id, change_status_to='enabled'):
"""Change checkbox status."""
checkbox = browser.find_by_id(checkbox_id)
checkbox.check() if change_status_to == 'enabled' else checkbox.uncheck()
interface.submit(browser, form_class='form-configuration')
if app_name in apps_with_loaders:
wait_for_config_update(browser, app_name)
def enable(browser, app_name):
interface.nav_to_module(browser, get_app_module(app_name))
_change_app_status(browser, app_name, 'enabled')
def disable(browser, app_name):
interface.nav_to_module(browser, get_app_module(app_name))
_change_app_status(browser, app_name, 'disabled')
def can_be_disabled(browser, app_name):
"""Return whether the application can be disabled."""
interface.nav_to_module(browser, get_app_module(app_name))
button = browser.find_by_css('button[name="app_enable_disable_button"]')
return bool(button)
def wait_for_config_update(browser, app_name):
while browser.is_element_present_by_css('.running-status.loading'):
sleep(0.1)
def _download_file(browser, url):
"""Return file contents after downloading a URL."""
cookies = browser.cookies.all()
response = requests.get(url, cookies=cookies, verify=False)
if response.status_code != 200:
raise Exception('URL download failed')
return response.content
def select_domain_name(browser, app_name, domain_name):
browser.visit('{}/plinth/apps/{}/setup/'.format(default_url, app_name))
drop_down = browser.find_by_id('id_domain_name')
drop_down.select(domain_name)
interface.submit(browser, form_class='form-configuration')
def configure_shadowsocks(browser, server, password):
"""Configure shadowsocks client with given server details."""
browser.visit('{}/plinth/apps/shadowsocks/'.format(default_url))
browser.find_by_id('id_server').fill(server)
browser.find_by_id('id_password').fill(password)
interface.submit(browser, form_class='form-configuration')
def shadowsocks_get_configuration(browser):
"""Return the server and password currently configured in shadowsocks."""
browser.visit('{}/plinth/apps/shadowsocks/'.format(default_url))
server = browser.find_by_id('id_server').value
password = browser.find_by_id('id_password').value
return server, password
def modify_max_file_size(browser, size):
"""Change the maximum file size of coquelicot to the given value"""
browser.visit('{}/plinth/apps/coquelicot/'.format(default_url))
browser.find_by_id('id_max_file_size').fill(size)
interface.submit(browser, form_class='form-configuration')
def get_max_file_size(browser):
"""Get the maximum file size of coquelicot"""
browser.visit('{}/plinth/apps/coquelicot/'.format(default_url))
return int(browser.find_by_id('id_max_file_size').value)
def modify_upload_password(browser, password):
"""Change the upload password for coquelicot to the given value"""
browser.visit('{}/plinth/apps/coquelicot/'.format(default_url))
browser.find_by_id('id_upload_password').fill(password)
interface.submit(browser, form_class='form-configuration')
# Sharing app helper functions
def remove_share(browser, name):
"""Remove a share in sharing app."""
try:
share_row = get_share(browser, name)
except splinter.exceptions.ElementDoesNotExist:
pass
else:
share_row.find_by_css('.share-remove')[0].click()
def add_share(browser, name, path, group):
"""Add a share in sharing app."""
browser.visit('{}/plinth/apps/sharing/add/'.format(default_url))
browser.fill('sharing-name', name)
browser.fill('sharing-path', path)
browser.find_by_css(
'#id_sharing-groups input[value="{}"]'.format(group)).check()
submit(browser)
def edit_share(browser, old_name, new_name, path, group):
"""Edit a share in sharing app."""
row = get_share(browser, old_name)
with wait_for_page_update(browser):
row.find_by_css('.share-edit')[0].click()
browser.fill('sharing-name', new_name)
browser.fill('sharing-path', path)
browser.find_by_css('#id_sharing-groups input').uncheck()
browser.find_by_css(
'#id_sharing-groups input[value="{}"]'.format(group)).check()
submit(browser)
def get_share(browser, name):
"""Return the row for a given share."""
browser.visit('{}/plinth/apps/sharing/'.format(default_url))
return browser.find_by_id('share-{}'.format(name))[0]
def verify_share(browser, name, path, group):
"""Verfiy that a share exists in list of shares."""
href = '{}/share/{}'.format(default_url, name)
url = '/share/{}'.format(name)
row = get_share(browser, name)
assert row.find_by_css('.share-name')[0].text == name
assert row.find_by_css('.share-path')[0].text == path
assert row.find_by_css('.share-url a')[0]['href'] == href
assert row.find_by_css('.share-url a')[0].text == url
assert row.find_by_css('.share-groups')[0].text == group
def access_share(browser, name):
"""Visit a share and see if it is accessible."""
row = get_share(browser, name)
url = row.find_by_css('.share-url a')[0]['href']
browser.visit(url)
assert '/share/{}'.format(name) in browser.title
def make_share_public(browser, name):
"""Make share publicly accessible."""
row = get_share(browser, name)
with wait_for_page_update(browser):
row.find_by_css('.share-edit')[0].click()
browser.find_by_id('id_sharing-is_public').check()
interface.submit(browser)
def verify_nonexistant_share(browser, name):
"""Verify that given URL for a given share name is a 404."""
url = '{}/share/{}'.format(default_url, name)
browser.visit(url)
assert '404' in browser.title
def verify_inaccessible_share(browser, name):
"""Verify that given URL for a given share name denies permission."""
url = '{}/share/{}'.format(default_url, name)
browser.visit(url)
eventually(lambda: '/plinth' in browser.url, args=[])
def enable_mediawiki_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'id_enable_public_registrations',
'enabled')
def disable_mediawiki_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'id_enable_public_registrations',
'disabled')
def enable_mediawiki_private_mode(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'id_enable_private_mode', 'enabled')
def disable_mediawiki_private_mode(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'id_enable_private_mode', 'disabled')
def set_mediawiki_admin_password(browser, password):
"""Set a password for the MediaWiki user called admin."""
interface.nav_to_module(browser, 'mediawiki')
browser.find_by_id('id_password').fill(password)
interface.submit(browser, form_class='form-configuration')
def enable_ejabberd_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
interface.nav_to_module(browser, 'ejabberd')
_change_status(browser, 'ejabberd', 'id_MAM_enabled', 'enabled')
def disable_ejabberd_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
interface.nav_to_module(browser, 'ejabberd')
_change_status(browser, 'ejabberd', 'id_MAM_enabled', 'disabled')
def ejabberd_add_contact(browser):
"""Add a contact to Ejabberd user's roster."""
site.jsxc_add_contact(browser)
def ejabberd_delete_contact(browser):
"""Delete the contact from Ejabberd user's roster."""
site.jsxc_delete_contact(browser)
def ejabberd_has_contact(browser):
"""Check whether the contact is in Ejabberd user's roster."""
return eventually(site.jsxc_has_contact, [browser])
def gitweb_create_repo(browser, repo, access=None, ok_if_exists=False):
"""Create repository."""
if not gitweb_repo_exists(browser, repo, access):
gitweb_delete_repo(browser, repo, ignore_missing=True)
browser.find_link_by_href('/plinth/apps/gitweb/create/').first.click()
browser.find_by_id('id_gitweb-name').fill(repo)
if access == 'private':
browser.find_by_id('id_gitweb-is_private').check()
elif access == 'public':
browser.find_by_id('id_gitweb-is_private').uncheck()
submit(browser)
elif not ok_if_exists:
assert False, 'Repo already exists.'
def gitweb_delete_repo(browser, repo, ignore_missing=False):
"""Delete repository."""
interface.nav_to_module(browser, 'gitweb')
delete_link = browser.find_link_by_href(
'/plinth/apps/gitweb/{}/delete/'.format(repo))
if delete_link or not ignore_missing:
delete_link.first.click()
submit(browser)
def gitweb_edit_repo_metadata(browser, repo, metadata):
"""Set repository metadata."""
interface.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
if 'name' in metadata:
browser.find_by_id('id_gitweb-name').fill(metadata['name'])
if 'description' in metadata:
browser.find_by_id('id_gitweb-description').fill(
metadata['description'])
if 'owner' in metadata:
browser.find_by_id('id_gitweb-owner').fill(metadata['owner'])
if 'access' in metadata:
if metadata['access'] == 'private':
browser.find_by_id('id_gitweb-is_private').check()
else:
browser.find_by_id('id_gitweb-is_private').uncheck()
submit(browser)
def gitweb_get_repo_metadata(browser, repo):
"""Get repository metadata."""
interface.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
metadata = {}
for item in ['name', 'description', 'owner']:
metadata[item] = browser.find_by_id('id_gitweb-' + item).value
if browser.find_by_id('id_gitweb-is_private').value:
metadata['access'] = 'private'
else:
metadata['access'] = 'public'
return metadata
def _gitweb_get_repo_url(repo, with_auth):
""""Get repository URL"""
scheme = 'http'
if default_url.startswith('https://'):
scheme = 'https'
url = default_url.split('://')[1] if '://' in default_url else default_url
password = 'gitweb_wrong_password'
if with_auth:
password = config['DEFAULT']['password']
return '{0}://{1}:{2}@{3}/gitweb/{4}'.format(scheme,
config['DEFAULT']['username'],
password, url, repo)
@contextlib.contextmanager
def _gitweb_temp_directory():
"""Create temporary directory"""
name = tempfile.mkdtemp(prefix='plinth_test_gitweb_')
yield name
shutil.rmtree(name)
def _gitweb_git_command_is_successful(command, cwd):
"""Check if a command runs successfully or gives authentication error"""
process = subprocess.run(command, capture_output=True, cwd=cwd)
if process.returncode != 0:
if 'Authentication failed' in process.stderr.decode():
return False
print(process.stdout.decode())
# raise exception
process.check_returncode()
return True
def gitweb_repo_exists(browser, repo, access=None):
"""Check whether the repository exists."""
interface.nav_to_module(browser, 'gitweb')
links_found = browser.find_link_by_href('/gitweb/{}.git'.format(repo))
access_matches = True
if links_found and access:
parent = links_found.first.find_by_xpath('..').first
private_icon = parent.find_by_css('.repo-private-icon')
if access == 'private':
access_matches = True if private_icon else False
if access == 'public':
access_matches = True if not private_icon else False
return bool(links_found) and access_matches
def gitweb_repo_is_readable(repo, with_auth=False, url_git_extension=False):
"""Check if a git repo is readable with git client."""
url = _gitweb_get_repo_url(repo, with_auth)
if url_git_extension:
url = url + '.git'
git_command = ['git', 'clone', '-c', 'http.sslverify=false', url]
with _gitweb_temp_directory() as cwd:
return _gitweb_git_command_is_successful(git_command, cwd)
def gitweb_repo_is_writable(repo, with_auth=False, url_git_extension=False):
"""Check if a git repo is writable with git client."""
url = _gitweb_get_repo_url(repo, with_auth)
if url_git_extension:
url = url + '.git'
with _gitweb_temp_directory() as cwd:
subprocess.run(['mkdir', 'test-project'], check=True, cwd=cwd)
cwd = os.path.join(cwd, 'test-project')
prepare_git_repo_commands = [
'git init -q', 'git config http.sslVerify false',
'git -c "user.name=Tester" -c "user.email=tester" '
'commit -q --allow-empty -m "test"'
]
for command in prepare_git_repo_commands:
subprocess.run(command, shell=True, check=True, cwd=cwd)
git_push_command = ['git', 'push', '-qf', url, 'master']
return _gitweb_git_command_is_successful(git_push_command, cwd)
def gitweb_set_repo_access(browser, repo, access):
"""Set repository as public or private."""
interface.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
if access == 'private':
browser.find_by_id('id_gitweb-is_private').check()
else:
browser.find_by_id('id_gitweb-is_private').uncheck()
submit(browser)
def gitweb_set_all_repos_private(browser):
"""Set all repositories private"""
interface.nav_to_module(browser, 'gitweb')
public_repos = []
for element in browser.find_by_css('#gitweb-repo-list .list-group-item'):
if not element.find_by_css('.repo-private-icon'):
repo = element.find_by_css('.repo-label').first.text
public_repos.append(repo)
for repo in public_repos:
gitweb_set_repo_access(browser, repo, 'private')
def gitweb_site_repo_exists(browser, repo):
"""Check whether the repository exists on Gitweb site."""
browser.visit('{}/gitweb'.format(default_url))
return browser.find_by_css('a[href="/gitweb/{0}.git"]'.format(repo))
def ikiwiki_create_wiki_if_needed(browser):
"""Create wiki if it does not exist."""
interface.nav_to_module(browser, 'ikiwiki')
wiki = browser.find_link_by_href('/ikiwiki/wiki')
if not wiki:
browser.find_link_by_href('/plinth/apps/ikiwiki/create/').first.click()
browser.find_by_id('id_ikiwiki-name').fill('wiki')
browser.find_by_id('id_ikiwiki-admin_name').fill(
config['DEFAULT']['username'])
browser.find_by_id('id_ikiwiki-admin_password').fill(
config['DEFAULT']['password'])
submit(browser)
def ikiwiki_delete_wiki(browser):
"""Delete wiki."""
interface.nav_to_module(browser, 'ikiwiki')
browser.find_link_by_href(
'/plinth/apps/ikiwiki/wiki/delete/').first.click()
submit(browser)
def ikiwiki_wiki_exists(browser):
"""Check whether the wiki exists."""
interface.nav_to_module(browser, 'ikiwiki')
wiki = browser.find_link_by_href('/ikiwiki/wiki')
return bool(wiki)
def time_zone_set(browser, time_zone):
"""Set the system time zone."""
interface.nav_to_module(browser, 'datetime')
browser.select('time_zone', time_zone)
interface.submit(browser, form_class='form-configuration')
def time_zone_get(browser):
"""Set the system time zone."""
interface.nav_to_module(browser, 'datetime')
return browser.find_by_name('time_zone').first.value
_TOR_FEATURE_TO_ELEMENT = {
'relay': 'tor-relay_enabled',
'bridge-relay': 'tor-bridge_relay_enabled',
'hidden-services': 'tor-hs_enabled',
'software': 'tor-apt_transport_tor_enabled'
}
def tor_feature_enable(browser, feature, should_enable):
"""Enable/disable a Tor feature."""
if not isinstance(should_enable, bool):
should_enable = should_enable in ('enable', 'enabled')
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
interface.nav_to_module(browser, 'tor')
checkbox_element = browser.find_by_name(element_name).first
if should_enable == checkbox_element.checked:
return
if should_enable:
if feature == 'bridge-relay':
browser.find_by_name('tor-relay_enabled').first.check()
checkbox_element.check()
else:
checkbox_element.uncheck()
interface.submit(browser, form_class='form-configuration')
wait_for_config_update(browser, 'tor')
def tor_assert_feature_enabled(browser, feature, enabled):
"""Assert whether Tor relay is enabled or disabled."""
if not isinstance(enabled, bool):
enabled = enabled in ('enable', 'enabled')
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
interface.nav_to_module(browser, 'tor')
assert browser.find_by_name(element_name).first.checked == enabled
def tor_get_relay_ports(browser):
"""Return the list of ports shown in the relay table."""
interface.nav_to_module(browser, 'tor')
return [
port_name.text
for port_name in browser.find_by_css('.tor-relay-port-name')
]
def tor_assert_hidden_services(browser):
"""Assert that hidden service information is shown."""
interface.nav_to_module(browser, 'tor')
assert browser.find_by_css('.tor-hs .tor-hs-hostname')
def tahoe_get_introducer(browser, domain, introducer_type):
"""Return an introducer element with a given type from tahoe-lafs."""
interface.nav_to_module(browser, 'tahoe')
css_class = '.{}-introducers .introducer-furl'.format(introducer_type)
for furl in browser.find_by_css(css_class):
if domain in furl.text:
return furl.parent
return None
def tahoe_add_introducer(browser, domain):
"""Add a new introducer into tahoe-lafs."""
interface.nav_to_module(browser, 'tahoe')
furl = 'pb://ewe4zdz6kxn7xhuvc7izj2da2gpbgeir@tcp:{}:3456/' \
'fko4ivfwgqvybppwar3uehkx6spaaou7'.format(domain)
browser.fill('pet_name', 'testintroducer')
browser.fill('furl', furl)
submit(browser, form_class='form-add-introducer')
def tahoe_remove_introducer(browser, domain):
"""Remove an introducer from tahoe-lafs."""
introducer = tahoe_get_introducer(browser, domain, 'connected')
submit(browser, element=introducer.find_by_css('.form-remove'))
def radicale_get_access_rights(browser):
access_rights_types = ['owner_only', 'owner_write', 'authenticated']
interface.nav_to_module(browser, 'radicale')
for access_rights_type in access_rights_types:
if browser.find_by_value(access_rights_type).checked:
return access_rights_type
def radicale_set_access_rights(browser, access_rights_type):
interface.nav_to_module(browser, 'radicale')
browser.choose('access_rights', access_rights_type)
interface.submit(browser, form_class='form-configuration')
def openvpn_setup(browser):
"""Setup the OpenVPN application after installation."""
interface.nav_to_module(browser, 'openvpn')
setup_form = browser.find_by_css('.form-setup')
if not setup_form:
return
submit(browser, form_class='form-setup')
wait_for_config_update(browser, 'openvpn')
def openvpn_download_profile(browser):
"""Download the current user's profile into a file and return path."""
interface.nav_to_module(browser, 'openvpn')
url = browser.find_by_css('.form-profile')['action']
return _download_file(browser, url)
def samba_set_share(browser, share_type, status='enabled'):
"""Enable or disable samba share."""
disk_name = 'disk'
share_type_name = '{0}_share'.format(share_type)
interface.nav_to_module(browser, 'samba')
for elem in browser.find_by_tag('td'):
if elem.text == disk_name:
share_form = elem.find_by_xpath('(..//*)[2]/form').first
share_btn = share_form.find_by_name(share_type_name).first
if status == 'enabled' and share_btn['value'] == 'enable':
share_btn.click()
elif status == 'disabled' and share_btn['value'] == 'disable':
share_btn.click()
break
def _samba_write_to_share(share_type, as_guest=False):
"""Write to the samba share, return output messages as string."""
disk_name = 'disk'
if share_type == 'open':
share_name = disk_name
else:
share_name = '{0}_{1}'.format(disk_name, share_type)
hostname = urllib.parse.urlparse(default_url).hostname
servicename = '\\\\{0}\\{1}'.format(hostname, share_name)
directory = '_plinth-test_{0}'.format(''.join(
random.SystemRandom().choices(string.ascii_letters, k=8)))
port = config['DEFAULT']['samba_port']
smb_command = ['smbclient', '-W', 'WORKGROUP', '-p', port]
if as_guest:
smb_command += ['-N']
else:
smb_command += [
'-U', '{0}%{1}'.format(config['DEFAULT']['username'],
config['DEFAULT']['password'])
]
smb_command += [
servicename, '-c', 'mkdir {0}; rmdir {0}'.format(directory)
]
return subprocess.check_output(smb_command).decode()
def samba_assert_share_is_writable(share_type, as_guest=False):
"""Assert that samba share is writable."""
output = _samba_write_to_share(share_type, as_guest=False)
assert not output, output
def samba_assert_share_is_not_accessible(share_type, as_guest=False):
"""Assert that samba share is not accessible."""
try:
_samba_write_to_share(share_type, as_guest)
except subprocess.CalledProcessError as err:
err_output = err.output.decode()
assert 'NT_STATUS_ACCESS_DENIED' in err_output, err_output
else:
assert False, 'Can access the share.'
def samba_assert_share_is_not_available(share_type):
"""Assert that samba share is not accessible."""
try:
_samba_write_to_share(share_type)
except subprocess.CalledProcessError as err:
err_output = err.output.decode()
assert 'NT_STATUS_BAD_NETWORK_NAME' in err_output, err_output
else:
assert False, 'Can access the share.'
def searx_enable_public_access(browser):
"""Enable Public Access in SearX"""
interface.nav_to_module(browser, 'searx')
browser.find_by_id('id_public_access').check()
interface.submit(browser, form_class='form-configuration')
def searx_disable_public_access(browser):
"""Enable Public Access in SearX"""
interface.nav_to_module(browser, 'searx')
browser.find_by_id('id_public_access').uncheck()
interface.submit(browser, form_class='form-configuration')
def find_on_front_page(browser, app_name):
browser.visit(default_url)
shortcuts = browser.find_link_by_href(f'/{app_name}/')
return shortcuts

View File

@ -1,150 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import logging
import random
import tempfile
import requests
from . import config
from .service import wait_for_page_update
sys_modules = [
'avahi', 'backups', 'bind', 'cockpit', 'config', 'datetime', 'diagnostics',
'dynamicdns', 'firewall', 'letsencrypt', 'monkeysphere', 'names',
'networks', 'pagekite', 'performance', 'power', 'security', 'snapshot',
'ssh', 'storage', 'upgrades', 'users'
]
default_url = config['DEFAULT']['url']
def login(browser, url, username, password):
# XXX: Find a way to remove the hardcoded jsxc URL
if '/plinth/' not in browser.url or '/jsxc/jsxc' in browser.url:
browser.visit(url)
apps_link = browser.find_link_by_href('/plinth/apps/')
if len(apps_link):
return
login_button = browser.find_link_by_href('/plinth/accounts/login/')
if login_button:
login_button.first.click()
if login_button:
browser.fill('username', username)
browser.fill('password', password)
submit(browser)
else:
browser.visit(default_url + '/plinth/firstboot/welcome')
submit(browser) # click the "Start Setup" button
create_admin_account(browser, username, password)
if '/network-topology-first-boot' in browser.url:
submit(browser, element=browser.find_by_name('skip')[0])
if '/internet-connection-type' in browser.url:
submit(browser, element=browser.find_by_name('skip')[0])
def is_login_prompt(browser):
return all(
[browser.find_by_id('id_username'),
browser.find_by_id('id_password')])
def nav_to_module(browser, module):
sys_or_apps = 'sys' if module in sys_modules else 'apps'
required_url = default_url + f'/plinth/{sys_or_apps}/{module}/'
if browser.url != required_url:
browser.visit(required_url)
def create_user(browser, name, password):
nav_to_module(browser, 'users')
with wait_for_page_update(browser):
browser.find_link_by_href('/plinth/sys/users/create/').first.click()
browser.find_by_id('id_username').fill(name)
browser.find_by_id('id_password1').fill(password)
browser.find_by_id('id_password2').fill(password)
submit(browser)
def rename_user(browser, old_name, new_name):
nav_to_module(browser, 'users')
with wait_for_page_update(browser):
browser.find_link_by_href('/plinth/sys/users/' + old_name +
'/edit/').first.click()
browser.find_by_id('id_username').fill(new_name)
submit(browser)
def delete_user(browser, name):
nav_to_module(browser, 'users')
delete_link = browser.find_link_by_href('/plinth/sys/users/' + name +
'/delete/')
if delete_link:
with wait_for_page_update(browser):
delete_link.first.click()
submit(browser)
def is_user(browser, name):
nav_to_module(browser, 'users')
edit_link = browser.find_link_by_href('/plinth/sys/users/' + name +
'/edit/')
return bool(edit_link)
def create_admin_account(browser, username, password):
browser.find_by_id('id_username').fill(username)
browser.find_by_id('id_password1').fill(password)
browser.find_by_id('id_password2').fill(password)
submit(browser)
def submit(browser, element=None, form_class=None, expected_url=None):
with wait_for_page_update(browser, expected_url=expected_url):
if element:
element.click()
elif form_class:
browser.find_by_css(
'.{} input[type=submit]'.format(form_class)).click()
else:
browser.find_by_css('input[type=submit]').click()
def create_sample_local_file():
"""Create a sample file for upload using browser."""
contents = bytearray(random.getrandbits(8) for _ in range(64))
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(contents)
return temp_file.name, contents
def download_file(url):
"""Download a file to disk given a URL."""
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
logging.captureWarnings(True)
request = requests.get(url, verify=False)
logging.captureWarnings(False)
temp_file.write(request.content)
return temp_file.name
def compare_files(file1, file2):
"""Assert that the contents of two files are the same."""
file1_contents = open(file1, 'rb').read()
file2_contents = open(file2, 'rb').read()
assert file1_contents == file2_contents
def go_to_status_logs(browser):
browser.visit(default_url + '/plinth/help/status-log/')
def are_status_logs_shown(browser):
return browser.is_text_present('Logs begin')

View File

@ -1,79 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import time
from contextlib import contextmanager
from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait
from . import interface
# unlisted services just use the service_name as module name
service_module = {
'ntp': 'datetime',
}
def get_service_module(service_name):
module = service_name
if service_name in service_module:
module = service_module[service_name]
return module
def is_running(browser, service_name):
interface.nav_to_module(browser, get_service_module(service_name))
return len(browser.find_by_id('service-not-running')) == 0
def is_not_running(browser, service_name):
interface.nav_to_module(browser, get_service_module(service_name))
return len(browser.find_by_id('service-not-running')) != 0
def eventually(function, args=[], timeout=30):
"""Execute a function returning a boolean expression till it returns
True or a timeout is reached"""
end_time = time.time() + timeout
current_time = time.time()
while current_time < end_time:
if function(*args):
return True
time.sleep(0.1)
current_time = time.time()
return False
@contextmanager
def wait_for_page_update(browser, timeout=300, expected_url=None):
page_body = browser.find_by_tag('body').first
yield
WebDriverWait(browser, timeout).until(page_loaded(page_body, expected_url))
class page_loaded():
"""
Wait until a page (re)loaded.
- element: Wait until this element gets stale
- expected_url (optional): Wait for the URL to become <expected_url>.
This can be necessary to wait for a redirect to finish.
"""
def __init__(self, element, expected_url=None):
self.element = element
self.expected_url = expected_url
def __call__(self, driver):
is_stale = False
try:
self.element.has_class('whatever_class')
except StaleElementReferenceException:
if self.expected_url is None:
is_stale = True
else:
if driver.url.endswith(self.expected_url):
is_stale = True
return is_stale

View File

@ -1,589 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import logging
import os
import pathlib
import time
import requests
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from . import application, config, interface, system
from .service import eventually, wait_for_page_update
# unlisted sites just use '/' + site_name as url
site_url = {
'wiki': '/ikiwiki',
'jsxc': '/plinth/apps/jsxc/jsxc/',
'cockpit': '/_cockpit/',
'syncthing': '/syncthing/',
}
def get_site_url(site_name):
if site_name.startswith('share'):
site_name = site_name.replace('_', '/')
url = '/' + site_name
if site_name in site_url:
url = site_url[site_name]
return url
def is_available(browser, site_name):
url_to_visit = config['DEFAULT']['url'] + get_site_url(site_name)
browser.visit(url_to_visit)
time.sleep(3)
browser.reload()
not_404 = '404' not in browser.title
# The site might have a default path after the sitename,
# e.g /mediawiki/Main_Page
no_redirect = browser.url.startswith(url_to_visit.strip('/'))
return not_404 and no_redirect
def access_url(browser, site_name):
browser.visit(config['DEFAULT']['url'] + get_site_url(site_name))
def verify_coquelicot_upload_password(browser, password):
browser.visit(config['DEFAULT']['url'] + '/coquelicot')
# ensure the password form is scrolled into view
browser.execute_script('window.scrollTo(100, 0)')
browser.find_by_id('upload_password').fill(password)
actions = ActionChains(browser.driver)
actions.send_keys(Keys.RETURN)
actions.perform()
assert eventually(browser.is_element_present_by_css,
args=['div[style*="display: none;"]'])
def upload_file_to_coquelicot(browser, file_path, password):
"""Upload a local file from disk to coquelicot."""
verify_coquelicot_upload_password(browser, password)
browser.attach_file('file', file_path)
interface.submit(browser)
assert eventually(browser.is_element_present_by_css,
args=['#content .url'])
url_textarea = browser.find_by_css('#content .url textarea').first
return url_textarea.value
def verify_mediawiki_create_account_link(browser):
browser.visit(config['DEFAULT']['url'] +
'/mediawiki/index.php/Special:CreateAccount')
assert eventually(browser.is_element_present_by_id,
args=['wpCreateaccount'])
def verify_mediawiki_no_create_account_link(browser):
browser.visit(config['DEFAULT']['url'] +
'/mediawiki/index.php/Special:CreateAccount')
assert eventually(browser.is_element_not_present_by_id,
args=['wpCreateaccount'])
def verify_mediawiki_anonymous_reads_edits_link(browser):
browser.visit(config['DEFAULT']['url'] + '/mediawiki')
assert eventually(browser.is_element_present_by_id, args=['ca-nstab-main'])
def verify_mediawiki_no_anonymous_reads_edits_link(browser):
browser.visit(config['DEFAULT']['url'] + '/mediawiki')
assert eventually(browser.is_element_not_present_by_id,
args=['ca-nstab-main'])
assert eventually(browser.is_element_present_by_id,
args=['ca-nstab-special'])
def _login_to_mediawiki(browser, username, password):
browser.visit(config['DEFAULT']['url'] +
'/mediawiki/index.php?title=Special:Login')
browser.find_by_id('wpName1').fill(username)
browser.find_by_id('wpPassword1').fill(password)
with wait_for_page_update(browser):
browser.find_by_id('wpLoginAttempt').click()
def login_to_mediawiki_with_credentials(browser, username, password):
_login_to_mediawiki(browser, username, password)
# Had to put it in the same step because sessions don't
# persist between steps
assert eventually(browser.is_element_present_by_id, args=['t-upload'])
def upload_image_mediawiki(browser, username, password, image):
"""Upload an image to MediaWiki. Idempotent."""
browser.visit(config['DEFAULT']['url'] + '/mediawiki')
_login_to_mediawiki(browser, username, password)
# Upload file
browser.visit(config['DEFAULT']['url'] + '/mediawiki/Special:Upload')
file_path = pathlib.Path(__file__).parent
file_path /= '../../static/themes/default/img/' + image
browser.attach_file('wpUploadFile', str(file_path.resolve()))
interface.submit(browser, element=browser.find_by_name('wpUpload')[0])
def get_number_of_uploaded_images_in_mediawiki(browser):
browser.visit(config['DEFAULT']['url'] + '/mediawiki/Special:ListFiles')
return len(browser.find_by_css('.TablePager_col_img_timestamp'))
def get_uploaded_image_in_mediawiki(browser, image):
browser.visit(config['DEFAULT']['url'] + '/mediawiki/Special:ListFiles')
elements = browser.find_link_by_partial_href(image)
return elements[0].value
def mediawiki_delete_main_page(browser):
"""Delete the mediawiki main page."""
_login_to_mediawiki(browser, 'admin', 'whatever123')
browser.visit(
'{}/mediawiki/index.php?title=Main_Page&action=delete'.format(
interface.default_url))
with wait_for_page_update(browser):
browser.find_by_id('wpConfirmB').first.click()
def mediawiki_has_main_page(browser):
"""Check if mediawiki main page exists."""
return eventually(_mediawiki_has_main_page, [browser])
def _mediawiki_has_main_page(browser):
"""Check if mediawiki main page exists."""
browser.visit('{}/mediawiki/Main_Page'.format(interface.default_url))
content = browser.find_by_id('mw-content-text').first
return 'This page has been deleted.' not in content.text
def jsxc_login(browser):
"""Login to JSXC."""
access_url(browser, 'jsxc')
browser.find_by_id('jsxc-username').fill(config['DEFAULT']['username'])
browser.find_by_id('jsxc-password').fill(config['DEFAULT']['password'])
browser.find_by_id('jsxc-submit').click()
relogin = browser.find_by_text('relogin')
if relogin:
relogin.first.click()
browser.find_by_id('jsxc_username').fill(config['DEFAULT']['username'])
browser.find_by_id('jsxc_password').fill(config['DEFAULT']['password'])
browser.find_by_text('Connect').first.click()
def jsxc_add_contact(browser):
"""Add a contact to JSXC user's roster."""
system.set_domain_name(browser, 'localhost')
application.install(browser, 'jsxc')
jsxc_login(browser)
new = browser.find_by_text('new contact')
if new: # roster is empty
new.first.click()
browser.find_by_id('jsxc_username').fill('alice@localhost')
browser.find_by_text('Add').first.click()
def jsxc_delete_contact(browser):
"""Delete the contact from JSXC user's roster."""
jsxc_login(browser)
browser.find_by_css('div.jsxc_more').first.click()
browser.find_by_text('delete contact').first.click()
browser.find_by_text('Remove').first.click()
def jsxc_has_contact(browser):
"""Check whether the contact is in JSXC user's roster."""
jsxc_login(browser)
contact = browser.find_by_text('alice@localhost')
return bool(contact)
def _mldonkey_submit_command(browser, command):
"""Submit a command to mldonkey."""
with browser.get_iframe('commands') as commands_frame:
commands_frame.find_by_css('.txt2').fill(command)
commands_frame.find_by_css('.but2').click()
def mldonkey_remove_all_ed2k_files(browser):
"""Remove all ed2k files from mldonkey."""
browser.visit(config['DEFAULT']['url'] + '/mldonkey/')
_mldonkey_submit_command(browser, 'cancel all')
_mldonkey_submit_command(browser, 'confirm yes')
def mldonkey_upload_sample_ed2k_file(browser):
"""Upload a sample ed2k file into mldonkey."""
browser.visit(config['DEFAULT']['url'] + '/mldonkey/')
dllink_command = 'dllink ed2k://|file|foo.bar|123|' \
'0123456789ABCDEF0123456789ABCDEF|/'
_mldonkey_submit_command(browser, dllink_command)
def mldonkey_get_number_of_ed2k_files(browser):
"""Return the number of ed2k files currently in mldonkey."""
browser.visit(config['DEFAULT']['url'] + '/mldonkey/')
with browser.get_iframe('commands') as commands_frame:
commands_frame.find_by_xpath(
'//tr//td[contains(text(), "Transfers")]').click()
with browser.get_iframe('output') as output_frame:
return len(output_frame.find_by_css('.dl-1')) + len(
output_frame.find_by_css('.dl-2'))
def transmission_remove_all_torrents(browser):
"""Remove all torrents from transmission."""
browser.visit(config['DEFAULT']['url'] + '/transmission')
while True:
torrents = browser.find_by_css('#torrent_list .torrent')
if not torrents:
break
torrents.first.click()
eventually(browser.is_element_not_present_by_css,
args=['#toolbar-remove.disabled'])
browser.click_link_by_id('toolbar-remove')
eventually(browser.is_element_not_present_by_css,
args=['#dialog-container[style="display: none;"]'])
browser.click_link_by_id('dialog_confirm_button')
eventually(browser.is_element_present_by_css,
args=['#toolbar-remove.disabled'])
def transmission_upload_sample_torrent(browser):
"""Upload a sample torrent into transmission."""
browser.visit(config['DEFAULT']['url'] + '/transmission')
file_path = os.path.join(os.path.dirname(__file__), '..', 'data',
'sample.torrent')
browser.click_link_by_id('toolbar-open')
eventually(browser.is_element_not_present_by_css,
args=['#upload-container[style="display: none;"]'])
browser.attach_file('torrent_files[]', [file_path])
browser.click_link_by_id('upload_confirm_button')
eventually(browser.is_element_present_by_css,
args=['#torrent_list .torrent'])
def transmission_get_number_of_torrents(browser):
"""Return the number torrents currently in transmission."""
browser.visit(config['DEFAULT']['url'] + '/transmission')
return len(browser.find_by_css('#torrent_list .torrent'))
def _deluge_get_active_window_title(browser):
"""Return the title of the currently active window in Deluge."""
return browser.evaluate_script(
'Ext.WindowMgr.getActive() ? Ext.WindowMgr.getActive().title : null')
def _deluge_ensure_logged_in(browser):
"""Ensure that password dialog is answered and we can interact."""
url = config['DEFAULT']['url'] + '/deluge'
def service_is_available():
if browser.is_element_present_by_xpath(
'//h1[text()="Service Unavailable"]'):
access_url(browser, 'deluge')
return False
return True
if browser.url != url:
browser.visit(url)
# After a backup restore, service may not be available immediately
eventually(service_is_available)
time.sleep(1) # Wait for Ext.js application in initialize
if _deluge_get_active_window_title(browser) != 'Login':
return
browser.find_by_id('_password').first.fill('deluge')
_deluge_click_active_window_button(browser, 'Login')
assert eventually(
lambda: _deluge_get_active_window_title(browser) != 'Login')
eventually(browser.is_element_not_present_by_css,
args=['#add.x-item-disabled'], timeout=0.3)
def _deluge_open_connection_manager(browser):
"""Open the connection manager dialog if not already open."""
title = 'Connection Manager'
if _deluge_get_active_window_title(browser) == title:
return
browser.find_by_css('button.x-deluge-connection-manager').first.click()
eventually(lambda: _deluge_get_active_window_title(browser) == title)
def _deluge_ensure_connected(browser):
"""Type the connection password if required and start Deluge daemon."""
_deluge_ensure_logged_in(browser)
# Change Default Password window appears once.
if _deluge_get_active_window_title(browser) == 'Change Default Password':
_deluge_click_active_window_button(browser, 'No')
assert eventually(browser.is_element_not_present_by_css,
args=['#add.x-item-disabled'])
def deluge_remove_all_torrents(browser):
"""Remove all torrents from deluge."""
_deluge_ensure_connected(browser)
while browser.find_by_css('#torrentGrid .torrent-name'):
browser.find_by_css('#torrentGrid .torrent-name').first.click()
# Click remove toolbar button
browser.find_by_id('remove').first.click()
# Remove window shows up
assert eventually(lambda: _deluge_get_active_window_title(browser) ==
'Remove Torrent')
_deluge_click_active_window_button(browser, 'Remove With Data')
# Remove window disappears
assert eventually(lambda: not _deluge_get_active_window_title(browser))
def _deluge_get_active_window_id(browser):
"""Return the ID of the currently active window."""
return browser.evaluate_script('Ext.WindowMgr.getActive().id')
def _deluge_click_active_window_button(browser, button_text):
"""Click an action button in the active window."""
browser.execute_script('''
active_window = Ext.WindowMgr.getActive();
active_window.buttons.forEach(function (button) {{
if (button.text == "{button_text}")
button.btnEl.dom.click()
}})'''.format(button_text=button_text))
def deluge_upload_sample_torrent(browser):
"""Upload a sample torrent into deluge."""
_deluge_ensure_connected(browser)
number_of_torrents = _deluge_get_number_of_torrents(browser)
# Click add toolbar button
browser.find_by_id('add').first.click()
# Add window appears
eventually(
lambda: _deluge_get_active_window_title(browser) == 'Add Torrents')
file_path = os.path.join(os.path.dirname(__file__), '..', 'data',
'sample.torrent')
if browser.find_by_id('fileUploadForm'): # deluge-web 2.x
browser.attach_file('file', file_path)
else: # deluge-web 1.x
browser.find_by_css('button.x-deluge-add-file').first.click()
# Add from file window appears
eventually(lambda: _deluge_get_active_window_title(browser) ==
'Add from File')
# Attach file
browser.attach_file('file', file_path)
# Click Add
_deluge_click_active_window_button(browser, 'Add')
eventually(
lambda: _deluge_get_active_window_title(browser) == 'Add Torrents')
# Click Add
time.sleep(1)
_deluge_click_active_window_button(browser, 'Add')
eventually(
lambda: _deluge_get_number_of_torrents(browser) > number_of_torrents)
def _deluge_get_number_of_torrents(browser):
"""Return the number torrents currently in deluge."""
return len(browser.find_by_css('#torrentGrid .torrent-name'))
def deluge_get_number_of_torrents(browser):
"""Return the number torrents currently in deluge."""
_deluge_ensure_connected(browser)
return _deluge_get_number_of_torrents(browser)
def calendar_is_available(browser):
"""Return whether calendar is available at well-known URL."""
conf = config['DEFAULT']
url = conf['url'] + '/.well-known/caldav'
logging.captureWarnings(True)
request = requests.get(url, auth=(conf['username'], conf['password']),
verify=False)
logging.captureWarnings(False)
return request.status_code != 404
def addressbook_is_available(browser):
"""Return whether addressbook is available at well-known URL."""
conf = config['DEFAULT']
url = conf['url'] + '/.well-known/carddav'
logging.captureWarnings(True)
request = requests.get(url, auth=(conf['username'], conf['password']),
verify=False)
logging.captureWarnings(False)
return request.status_code != 404
def _syncthing_load_main_interface(browser):
"""Close the dialog boxes that many popup after visiting the URL."""
access_url(browser, 'syncthing')
def service_is_available():
if browser.is_element_present_by_xpath(
'//h1[text()="Service Unavailable"]'):
access_url(browser, 'syncthing')
return False
return True
# After a backup restore, service may not be available immediately
eventually(service_is_available)
# Wait for javascript loading process to complete
browser.execute_script('''
document.is_ui_online = false;
var old_console_log = console.log;
console.log = function(message) {
old_console_log.apply(null, arguments);
if (message == 'UIOnline') {
document.is_ui_online = true;
console.log = old_console_log;
}
};
''')
eventually(lambda: browser.evaluate_script('document.is_ui_online'),
timeout=5)
# Dismiss the Usage Reporting consent dialog
usage_reporting = browser.find_by_id('ur').first
eventually(lambda: usage_reporting.visible, timeout=2)
if usage_reporting.visible:
yes_xpath = './/button[contains(@ng-click, "declineUR")]'
usage_reporting.find_by_xpath(yes_xpath).first.click()
eventually(lambda: not usage_reporting.visible)
def syncthing_folder_is_present(browser, folder_name):
"""Return whether a folder is present in Syncthing."""
_syncthing_load_main_interface(browser)
folder_names = browser.find_by_css('#folders .panel-title-text span')
folder_names = [folder_name.text for folder_name in folder_names]
return folder_name in folder_names
def syncthing_add_folder(browser, folder_name, folder_path):
"""Add a new folder to Synthing."""
_syncthing_load_main_interface(browser)
add_folder_xpath = '//button[contains(@ng-click, "addFolder")]'
browser.find_by_xpath(add_folder_xpath).click()
folder_dialog = browser.find_by_id('editFolder').first
eventually(lambda: folder_dialog.visible)
browser.find_by_id('folderLabel').fill(folder_name)
browser.find_by_id('folderPath').fill(folder_path)
save_folder_xpath = './/button[contains(@ng-click, "saveFolder")]'
folder_dialog.find_by_xpath(save_folder_xpath).first.click()
eventually(lambda: not folder_dialog.visible)
def syncthing_remove_folder(browser, folder_name):
"""Remove a folder from Synthing."""
_syncthing_load_main_interface(browser)
# Find folder
folder = None
for current_folder in browser.find_by_css('#folders > .panel'):
name = current_folder.find_by_css('.panel-title-text span').first.text
if name == folder_name:
folder = current_folder
break
# Edit folder button
folder.find_by_css('button.panel-heading').first.click()
eventually(lambda: folder.find_by_css('div.collapse.in'))
edit_folder_xpath = './/button[contains(@ng-click, "editFolder")]'
edit_folder_button = folder.find_by_xpath(edit_folder_xpath).first
edit_folder_button.click()
# Edit folder dialog
folder_dialog = browser.find_by_id('editFolder').first
eventually(lambda: folder_dialog.visible)
remove_button_xpath = './/button[contains(@data-target, "remove-folder")]'
folder_dialog.find_by_xpath(remove_button_xpath).first.click()
# Remove confirmation dialog
remove_folder_dialog = browser.find_by_id('remove-folder-confirmation')
eventually(lambda: remove_folder_dialog.visible)
remove_button_xpath = './/button[contains(@ng-click, "deleteFolder")]'
remove_folder_dialog.find_by_xpath(remove_button_xpath).first.click()
eventually(lambda: not folder_dialog.visible)
def _ttrss_load_main_interface(browser):
"""Load the TT-RSS interface."""
access_url(browser, 'tt-rss')
overlay = browser.find_by_id('overlay')
eventually(lambda: not overlay.visible)
def _ttrss_is_feed_shown(browser, invert=False):
return browser.is_text_present('Planet Debian') != invert
def ttrss_subscribe(browser):
"""Subscribe to a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
browser.find_by_text('Actions...').click()
browser.find_by_text('Subscribe to feed...').click()
browser.find_by_id('feedDlg_feedUrl').fill(
'https://planet.debian.org/atom.xml')
browser.find_by_text('Subscribe').click()
if browser.is_text_present('You are already subscribed to this feed.'):
browser.find_by_text('Cancel').click()
expand = browser.find_by_css('span.dijitTreeExpandoClosed')
if expand:
expand.first.click()
assert eventually(_ttrss_is_feed_shown, [browser])
def ttrss_unsubscribe(browser):
"""Unsubscribe from a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
expand = browser.find_by_css('span.dijitTreeExpandoClosed')
if expand:
expand.first.click()
browser.find_by_text('Planet Debian').click()
browser.execute_script("quickMenuGo('qmcRemoveFeed')")
prompt = browser.get_alert()
prompt.accept()
assert eventually(_ttrss_is_feed_shown, [browser, True])
def ttrss_is_subscribed(browser):
"""Return whether subscribed to a feed in TT-RSS."""
_ttrss_load_main_interface(browser)
return browser.is_text_present('Planet Debian')

View File

@ -1,417 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import tempfile
from urllib.parse import urlparse
import requests
from . import application, config
from .interface import nav_to_module, submit
from .service import wait_for_page_update
config_page_title_language_map = {
'da': 'Generel Konfiguration',
'de': 'Allgemeine Konfiguration',
'es': 'Configuración general',
'fr': 'Configuration générale',
'nb': 'Generelt oppsett',
'nl': 'Algemene Instellingen',
'pl': 'Ustawienia główne',
'pt': 'Configuração Geral',
'ru': 'Общие настройки',
'sv': 'Allmän Konfiguration',
'te': 'సాధారణ ఆకృతీకరణ',
'tr': 'Genel Yapılandırma',
'zh-hans': '常规配置',
}
def get_hostname(browser):
nav_to_module(browser, 'config')
return browser.find_by_id('id_hostname').value
def set_hostname(browser, hostname):
nav_to_module(browser, 'config')
browser.find_by_id('id_hostname').fill(hostname)
submit(browser)
def get_domain_name(browser):
nav_to_module(browser, 'config')
return browser.find_by_id('id_domainname').value
def set_domain_name(browser, domain_name):
nav_to_module(browser, 'config')
browser.find_by_id('id_domainname').fill(domain_name)
submit(browser)
def set_home_page(browser, home_page):
if 'plinth' not in home_page and 'apache' not in home_page:
home_page = 'shortcut-' + home_page
nav_to_module(browser, 'config')
drop_down = browser.find_by_id('id_homepage')
drop_down.select(home_page)
submit(browser)
def set_advanced_mode(browser, mode):
nav_to_module(browser, 'config')
advanced_mode = browser.find_by_id('id_advanced_mode')
if mode:
advanced_mode.check()
else:
advanced_mode.uncheck()
submit(browser)
def set_language(browser, language_code):
username = config['DEFAULT']['username']
browser.visit(config['DEFAULT']['url'] +
'/plinth/sys/users/{}/edit/'.format(username))
browser.find_by_xpath('//select[@id="id_language"]//option[@value="' +
language_code + '"]').first.click()
submit(browser)
def check_language(browser, language_code):
nav_to_module(browser, 'config')
return browser.find_by_css('.app-titles').first.find_by_tag(
'h2').first.value == config_page_title_language_map[language_code]
def delete_all_snapshots(browser):
if get_snapshot_count(browser):
browser.find_by_id('select-all').check()
submit(browser, browser.find_by_name('delete_selected'))
confirm_button = browser.find_by_name('delete_confirm')
if confirm_button: # Only if redirected to confirm page
submit(browser, confirm_button)
def create_snapshot(browser):
browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/manage/')
submit(browser) # Click on 'Create Snapshot'
def get_snapshot_count(browser):
browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/manage/')
# Subtract 1 for table header
return len(browser.find_by_xpath('//tr')) - 1
def snapshot_set_configuration(browser, free_space, timeline_enabled,
software_enabled, hourly, daily, weekly,
monthly, yearly):
"""Set the configuration for snapshots."""
nav_to_module(browser, 'snapshot')
browser.find_by_name('free_space').select(free_space / 100)
browser.find_by_name('enable_timeline_snapshots').select(
'yes' if timeline_enabled else 'no')
browser.find_by_name('enable_software_snapshots').select(
'yes' if software_enabled else 'no')
browser.find_by_name('hourly_limit').fill(hourly)
browser.find_by_name('daily_limit').fill(daily)
browser.find_by_name('weekly_limit').fill(weekly)
browser.find_by_name('monthly_limit').fill(monthly)
browser.find_by_name('yearly_limit').fill(yearly)
submit(browser)
def snapshot_get_configuration(browser):
"""Return the current configuration for snapshots."""
nav_to_module(browser, 'snapshot')
return (int(float(browser.find_by_name('free_space').value) * 100),
browser.find_by_name('enable_timeline_snapshots').value == 'yes',
browser.find_by_name('enable_software_snapshots').value == 'yes',
int(browser.find_by_name('hourly_limit').value),
int(browser.find_by_name('daily_limit').value),
int(browser.find_by_name('weekly_limit').value),
int(browser.find_by_name('monthly_limit').value),
int(browser.find_by_name('yearly_limit').value))
def check_home_page_redirect(browser, app_name):
browser.visit(config['DEFAULT']['url'])
return browser.find_by_xpath(
"//a[contains(@href, '/plinth/') and @title='FreedomBox']")
def dynamicdns_configure(browser):
nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
browser.find_by_id('id_enabled').check()
browser.find_by_id('id_service_type').select('GnuDIP')
browser.find_by_id('id_dynamicdns_server').fill('example.com')
browser.find_by_id('id_dynamicdns_domain').fill('freedombox.example.com')
browser.find_by_id('id_dynamicdns_user').fill('tester')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip.datasystems24.de')
submit(browser)
def dynamicdns_has_original_config(browser):
nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
enabled = browser.find_by_id('id_enabled').value
service_type = browser.find_by_id('id_service_type').value
server = browser.find_by_id('id_dynamicdns_server').value
domain = browser.find_by_id('id_dynamicdns_domain').value
user = browser.find_by_id('id_dynamicdns_user').value
ipurl = browser.find_by_id('id_dynamicdns_ipurl').value
if enabled and service_type == 'GnuDIP' and server == 'example.com' \
and domain == 'freedombox.example.com' and user == 'tester' \
and ipurl == 'http://myip.datasystems24.de':
return True
else:
return False
def dynamicdns_change_config(browser):
nav_to_module(browser, 'dynamicdns')
browser.find_link_by_href(
'/plinth/sys/dynamicdns/configure/').first.click()
browser.find_by_id('id_enabled').check()
browser.find_by_id('id_service_type').select('GnuDIP')
browser.find_by_id('id_dynamicdns_server').fill('2.example.com')
browser.find_by_id('id_dynamicdns_domain').fill('freedombox2.example.com')
browser.find_by_id('id_dynamicdns_user').fill('tester2')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting2')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip2.datasystems24.de')
submit(browser)
def _click_button_and_confirm(browser, href):
buttons = browser.find_link_by_href(href)
if buttons:
buttons.first.click()
with wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
submit(browser)
def backup_delete_archive_by_name(browser, archive_name):
nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/delete/{archive_name}/'
_click_button_and_confirm(browser, href)
def backup_create(browser, app_name, archive_name=None):
application.install(browser, 'backups')
if archive_name:
backup_delete_archive_by_name(browser, archive_name)
browser.find_link_by_href('/plinth/sys/backups/create/').first.click()
browser.find_by_id('select-all').uncheck()
if archive_name:
browser.find_by_id('id_backups-name').fill(archive_name)
# ensure the checkbox is scrolled into view
browser.execute_script('window.scrollTo(0, 0)')
browser.find_by_value(app_name).first.check()
submit(browser)
def backup_restore(browser, app_name, archive_name=None):
nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/restore-archive/{archive_name}/'
_click_button_and_confirm(browser, href)
def backup_upload_and_restore(browser, app_name, downloaded_file_path):
nav_to_module(browser, 'backups')
browser.find_link_by_href('/plinth/sys/backups/upload/').first.click()
fileinput = browser.driver.find_element_by_id('id_backups-file')
fileinput.send_keys(downloaded_file_path)
# submit upload form
submit(browser)
# submit restore form
with wait_for_page_update(browser, expected_url='/plinth/sys/backups/'):
submit(browser)
def download_backup(browser, archive_name=None):
nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/download/{archive_name}/'
url = config['DEFAULT']['url'] + href
file_path = download_file_logged_in(browser, url, suffix='.tar.gz')
return file_path
def download_file_logged_in(browser, url, suffix=''):
"""Download a file from Plinth, pretend being logged in via cookies"""
if not url.startswith("http"):
current_url = urlparse(browser.url)
url = "%s://%s%s" % (current_url.scheme, current_url.netloc, url)
cookies = browser.driver.get_cookies()
cookies = {cookie["name"]: cookie["value"] for cookie in cookies}
response = requests.get(url, verify=False, cookies=cookies)
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
for chunk in response.iter_content(chunk_size=128):
temp_file.write(chunk)
return temp_file.name
def pagekite_configure(browser, host, port, kite_name, kite_secret):
"""Configure pagekite basic parameters."""
nav_to_module(browser, 'pagekite')
# time.sleep(0.250) # Wait for 200ms show animation to complete
browser.fill('pagekite-server_domain', host)
browser.fill('pagekite-server_port', str(port))
browser.fill('pagekite-kite_name', kite_name)
browser.fill('pagekite-kite_secret', kite_secret)
submit(browser, form_class='form-configuration')
def pagekite_get_configuration(browser):
"""Return pagekite basic parameters."""
nav_to_module(browser, 'pagekite')
return (browser.find_by_name('pagekite-server_domain').value,
int(browser.find_by_name('pagekite-server_port').value),
browser.find_by_name('pagekite-kite_name').value,
browser.find_by_name('pagekite-kite_secret').value)
def bind_set_forwarders(browser, forwarders):
"""Set the forwarders list (space separated) in bind configuration."""
nav_to_module(browser, 'bind')
browser.fill('forwarders', forwarders)
submit(browser, form_class='form-configuration')
def bind_get_forwarders(browser):
"""Return the forwarders list (space separated) in bind configuration."""
nav_to_module(browser, 'bind')
return browser.find_by_name('forwarders').first.value
def bind_enable_dnssec(browser, enable):
"""Enable/disable DNSSEC in bind configuration."""
nav_to_module(browser, 'bind')
if enable:
browser.check('enable_dnssec')
else:
browser.uncheck('enable_dnssec')
submit(browser, form_class='form-configuration')
def bind_get_dnssec(browser):
"""Return whether DNSSEC is enabled/disabled in bind configuration."""
nav_to_module(browser, 'bind')
return browser.find_by_name('enable_dnssec').first.checked
def security_enable_restricted_logins(browser, should_enable):
"""Enable/disable restricted logins in security module."""
nav_to_module(browser, 'security')
if should_enable:
browser.check('security-restricted_access')
else:
browser.uncheck('security-restricted_access')
submit(browser)
def security_get_restricted_logins(browser):
"""Return whether restricted console logins is enabled."""
nav_to_module(browser, 'security')
return browser.find_by_name('security-restricted_access').first.checked
def upgrades_enable_automatic(browser, should_enable):
"""Enable/disable automatic software upgrades."""
nav_to_module(browser, 'upgrades')
checkbox_element = browser.find_by_name('auto_upgrades_enabled').first
if should_enable == checkbox_element.checked:
return
if should_enable:
checkbox_element.check()
else:
checkbox_element.uncheck()
submit(browser)
def upgrades_get_automatic(browser):
"""Return whether automatic software upgrades is enabled."""
nav_to_module(browser, 'upgrades')
return browser.find_by_name('auto_upgrades_enabled').first.checked
def _monkeysphere_find_domain(browser, key_type, domain_type, domain):
"""Iterate every domain of a given type which given key type."""
keys_of_type = browser.find_by_css(
'.monkeysphere-service-{}'.format(key_type))
for key_of_type in keys_of_type:
search_domains = key_of_type.find_by_css(
'.monkeysphere-{}-domain'.format(domain_type))
for search_domain in search_domains:
if search_domain.text == domain:
return key_of_type, search_domain
raise IndexError('Domain not found')
def monkeysphere_import_key(browser, key_type, domain):
"""Import a key of specified type for given domain into monkeysphere."""
try:
monkeysphere_assert_imported_key(browser, key_type, domain)
except IndexError:
pass
else:
return
key, _ = _monkeysphere_find_domain(browser, key_type, 'importable', domain)
with wait_for_page_update(browser):
key.find_by_css('.button-import').click()
def monkeysphere_assert_imported_key(browser, key_type, domain):
"""Assert that a key of specified type for given domain was imported.."""
nav_to_module(browser, 'monkeysphere')
return _monkeysphere_find_domain(browser, key_type, 'imported', domain)
def monkeysphere_publish_key(browser, key_type, domain):
"""Publish a key of specified type for given domain from monkeysphere."""
nav_to_module(browser, 'monkeysphere')
key, _ = _monkeysphere_find_domain(browser, key_type, 'imported', domain)
with wait_for_page_update(browser):
key.find_by_css('.button-publish').click()
application.wait_for_config_update(browser, 'monkeysphere')
def open_main_page(browser):
with wait_for_page_update(browser):
browser.find_link_by_href('/plinth/').first.click()
def networks_set_firewall_zone(browser, zone):
""""Set the network device firewall zone as internal or external."""
nav_to_module(browser, 'networks')
device = browser.find_by_xpath(
'//span[contains(@class, "label-success") '
'and contains(@class, "connection-status-label")]/following::a').first
network_id = device['href'].split('/')[-3]
device.click()
edit_url = "/plinth/sys/networks/{}/edit/".format(network_id)
browser.find_link_by_href(edit_url).first.click()
browser.select('zone', zone)
browser.find_by_tag("form").first.find_by_tag('input')[-1].click()
def storage_is_root_disk_shown(browser):
table_cells = browser.find_by_tag('td')
return any(cell.text == '/' for cell in table_cells)