diff --git a/conftest.py b/conftest.py index c70aa0843..873409358 100644 --- a/conftest.py +++ b/conftest.py @@ -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): diff --git a/plinth/modules/avahi/tests/avahi.feature b/plinth/modules/avahi/tests/avahi.feature index c8606dec6..0ab01d1cb 100644 --- a/plinth/modules/avahi/tests/avahi.feature +++ b/plinth/modules/avahi/tests/avahi.feature @@ -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 diff --git a/plinth/modules/backups/tests/test_functional.py b/plinth/modules/backups/tests/test_functional.py index 4c6a3171f..535c387bc 100644 --- a/plinth/modules/backups/tests/test_functional.py +++ b/plinth/modules/backups/tests/test_functional.py @@ -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) diff --git a/plinth/modules/bind/tests/test_functional.py b/plinth/modules/bind/tests/test_functional.py index 4c5e69b5b..78e47183a 100644 --- a/plinth/modules/bind/tests/test_functional.py +++ b/plinth/modules/bind/tests/test_functional.py @@ -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 diff --git a/plinth/modules/config/tests/config.feature b/plinth/modules/config/tests/config.feature index bdddf4e61..eb90d751c 100644 --- a/plinth/modules/config/tests/config.feature +++ b/plinth/modules/config/tests/config.feature @@ -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 - diff --git a/plinth/modules/config/tests/test_functional.py b/plinth/modules/config/tests/test_functional.py index 84479a4f2..d1d79f74c 100644 --- a/plinth/modules/config/tests/test_functional.py +++ b/plinth/modules/config/tests/test_functional.py @@ -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']") diff --git a/plinth/modules/coquelicot/tests/test_functional.py b/plinth/modules/coquelicot/tests/test_functional.py index b9eb4da65..e8c8a08fe 100644 --- a/plinth/modules/coquelicot/tests/test_functional.py +++ b/plinth/modules/coquelicot/tests/test_functional.py @@ -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 diff --git a/plinth/modules/datetime/tests/datetime.feature b/plinth/modules/datetime/tests/datetime.feature index e36b75f5e..718e51992 100644 --- a/plinth/modules/datetime/tests/datetime.feature +++ b/plinth/modules/datetime/tests/datetime.feature @@ -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 diff --git a/plinth/modules/datetime/tests/test_functional.py b/plinth/modules/datetime/tests/test_functional.py index de8e87c77..8d4921f88 100644 --- a/plinth/modules/datetime/tests/test_functional.py +++ b/plinth/modules/datetime/tests/test_functional.py @@ -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 diff --git a/plinth/tests/functional/data/sample.torrent b/plinth/modules/deluge/tests/data/sample.torrent similarity index 100% rename from plinth/tests/functional/data/sample.torrent rename to plinth/modules/deluge/tests/data/sample.torrent diff --git a/plinth/modules/deluge/tests/test_functional.py b/plinth/modules/deluge/tests/test_functional.py index 11f4c84e8..b2b8d88df 100644 --- a/plinth/modules/deluge/tests/test_functional.py +++ b/plinth/modules/deluge/tests/test_functional.py @@ -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')) diff --git a/plinth/modules/dynamicdns/tests/test_functional.py b/plinth/modules/dynamicdns/tests/test_functional.py index 5438d0c4e..276c487e6 100644 --- a/plinth/modules/dynamicdns/tests/test_functional.py +++ b/plinth/modules/dynamicdns/tests/test_functional.py @@ -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) diff --git a/plinth/modules/ejabberd/tests/test_functional.py b/plinth/modules/ejabberd/tests/test_functional.py index 5564dd8b7..7a2755019 100644 --- a/plinth/modules/ejabberd/tests/test_functional.py +++ b/plinth/modules/ejabberd/tests/test_functional.py @@ -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) diff --git a/plinth/modules/gitweb/tests/test_functional.py b/plinth/modules/gitweb/tests/test_functional.py index df1f610c9..2dbbbaf8c 100644 --- a/plinth/modules/gitweb/tests/test_functional.py +++ b/plinth/modules/gitweb/tests/test_functional.py @@ -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)) diff --git a/plinth/modules/help/tests/test_functional.py b/plinth/modules/help/tests/test_functional.py index e62284ce8..dcdc66b9d 100644 --- a/plinth/modules/help/tests/test_functional.py +++ b/plinth/modules/help/tests/test_functional.py @@ -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') diff --git a/plinth/modules/ikiwiki/tests/ikiwiki.feature b/plinth/modules/ikiwiki/tests/ikiwiki.feature index 7b6ab085b..d70056851 100644 --- a/plinth/modules/ikiwiki/tests/ikiwiki.feature +++ b/plinth/modules/ikiwiki/tests/ikiwiki.feature @@ -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 diff --git a/plinth/modules/ikiwiki/tests/test_functional.py b/plinth/modules/ikiwiki/tests/test_functional.py index c8a67665f..9f173973a 100644 --- a/plinth/modules/ikiwiki/tests/test_functional.py +++ b/plinth/modules/ikiwiki/tests/test_functional.py @@ -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) diff --git a/plinth/modules/mediawiki/tests/test_functional.py b/plinth/modules/mediawiki/tests/test_functional.py index 62c41b3a5..626dfbd1b 100644 --- a/plinth/modules/mediawiki/tests/test_functional.py +++ b/plinth/modules/mediawiki/tests/test_functional.py @@ -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 diff --git a/plinth/modules/mldonkey/tests/test_functional.py b/plinth/modules/mldonkey/tests/test_functional.py index 4bc0d706d..5a53cc855 100644 --- a/plinth/modules/mldonkey/tests/test_functional.py +++ b/plinth/modules/mldonkey/tests/test_functional.py @@ -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')) diff --git a/plinth/modules/monkeysphere/tests/test_functional.py b/plinth/modules/monkeysphere/tests/test_functional.py index f475b9320..45ab8383d 100644 --- a/plinth/modules/monkeysphere/tests/test_functional.py +++ b/plinth/modules/monkeysphere/tests/test_functional.py @@ -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') diff --git a/plinth/modules/openvpn/tests/test_functional.py b/plinth/modules/openvpn/tests/test_functional.py index 176632bec..2c5fa58d8 100644 --- a/plinth/modules/openvpn/tests/test_functional.py +++ b/plinth/modules/openvpn/tests/test_functional.py @@ -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) diff --git a/plinth/modules/pagekite/tests/test_functional.py b/plinth/modules/pagekite/tests/test_functional.py index 227edad44..d18168619 100644 --- a/plinth/modules/pagekite/tests/test_functional.py +++ b/plinth/modules/pagekite/tests/test_functional.py @@ -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) diff --git a/plinth/modules/radicale/tests/test_functional.py b/plinth/modules/radicale/tests/test_functional.py index 5af778039..7b3a29463 100644 --- a/plinth/modules/radicale/tests/test_functional.py +++ b/plinth/modules/radicale/tests/test_functional.py @@ -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 diff --git a/plinth/modules/samba/tests/test_functional.py b/plinth/modules/samba/tests/test_functional.py index 237a1171c..5b2319fb0 100644 --- a/plinth/modules/samba/tests/test_functional.py +++ b/plinth/modules/samba/tests/test_functional.py @@ -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.' diff --git a/plinth/modules/searx/tests/test_functional.py b/plinth/modules/searx/tests/test_functional.py index 459de6735..e24ca2548 100644 --- a/plinth/modules/searx/tests/test_functional.py +++ b/plinth/modules/searx/tests/test_functional.py @@ -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') diff --git a/plinth/modules/security/tests/test_functional.py b/plinth/modules/security/tests/test_functional.py index af98e32d5..c0266b111 100644 --- a/plinth/modules/security/tests/test_functional.py +++ b/plinth/modules/security/tests/test_functional.py @@ -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 diff --git a/plinth/modules/shadowsocks/tests/test_functional.py b/plinth/modules/shadowsocks/tests/test_functional.py index 3ee9d1237..3e6095690 100644 --- a/plinth/modules/shadowsocks/tests/test_functional.py +++ b/plinth/modules/shadowsocks/tests/test_functional.py @@ -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 diff --git a/plinth/modules/sharing/tests/test_functional.py b/plinth/modules/sharing/tests/test_functional.py index 974840cec..7ce837e68 100644 --- a/plinth/modules/sharing/tests/test_functional.py +++ b/plinth/modules/sharing/tests/test_functional.py @@ -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=[]) diff --git a/plinth/modules/snapshot/tests/test_functional.py b/plinth/modules/snapshot/tests/test_functional.py index 4fc5aee8d..0eecf3c53 100644 --- a/plinth/modules/snapshot/tests/test_functional.py +++ b/plinth/modules/snapshot/tests/test_functional.py @@ -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)) diff --git a/plinth/modules/storage/tests/test_functional.py b/plinth/modules/storage/tests/test_functional.py index 737f32ef1..aa1cce7e1 100644 --- a/plinth/modules/storage/tests/test_functional.py +++ b/plinth/modules/storage/tests/test_functional.py @@ -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) diff --git a/plinth/modules/syncthing/tests/test_functional.py b/plinth/modules/syncthing/tests/test_functional.py index eac5eb009..6709948d1 100644 --- a/plinth/modules/syncthing/tests/test_functional.py +++ b/plinth/modules/syncthing/tests/test_functional.py @@ -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) diff --git a/plinth/modules/tahoe/tests/test_functional.py b/plinth/modules/tahoe/tests/test_functional.py index 899cc62c2..c6ebedb29 100644 --- a/plinth/modules/tahoe/tests/test_functional.py +++ b/plinth/modules/tahoe/tests/test_functional.py @@ -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')) diff --git a/plinth/modules/tor/tests/test_functional.py b/plinth/modules/tor/tests/test_functional.py index bad412e97..ae159ca46 100644 --- a/plinth/modules/tor/tests/test_functional.py +++ b/plinth/modules/tor/tests/test_functional.py @@ -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') diff --git a/plinth/modules/transmission/tests/data/sample.torrent b/plinth/modules/transmission/tests/data/sample.torrent new file mode 100644 index 000000000..4c2ed6bf4 Binary files /dev/null and b/plinth/modules/transmission/tests/data/sample.torrent differ diff --git a/plinth/modules/transmission/tests/test_functional.py b/plinth/modules/transmission/tests/test_functional.py index 1ece1c1b3..ae7236b2e 100644 --- a/plinth/modules/transmission/tests/test_functional.py +++ b/plinth/modules/transmission/tests/test_functional.py @@ -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')) diff --git a/plinth/modules/ttrss/tests/test_functional.py b/plinth/modules/ttrss/tests/test_functional.py index a94acba7b..ef44d5598 100644 --- a/plinth/modules/ttrss/tests/test_functional.py +++ b/plinth/modules/ttrss/tests/test_functional.py @@ -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') diff --git a/plinth/modules/upgrades/tests/test_functional.py b/plinth/modules/upgrades/tests/test_functional.py index 272599b14..48e2c60fd 100644 --- a/plinth/modules/upgrades/tests/test_functional.py +++ b/plinth/modules/upgrades/tests/test_functional.py @@ -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 diff --git a/plinth/modules/users/tests/test_functional.py b/plinth/modules/users/tests/test_functional.py index 92b26c540..02cfd78dc 100644 --- a/plinth/modules/users/tests/test_functional.py +++ b/plinth/modules/users/tests/test_functional.py @@ -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 ') +def change_language(session_browser, language): + _set_language(session_browser, _language_codes[language]) + + +@then('Plinth language should be ') +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] diff --git a/plinth/modules/users/tests/users.feature b/plinth/modules/users/tests/users.feature index 52ce217f0..fcebebd10 100644 --- a/plinth/modules/users/tests/users.feature +++ b/plinth/modules/users/tests/users.feature @@ -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. diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index e69de29bb..2f757e2f5 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -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 . + 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 diff --git a/plinth/tests/functional/step_definitions.py b/plinth/tests/functional/step_definitions.py new file mode 100644 index 000000000..034c002af --- /dev/null +++ b/plinth/tests/functional/step_definitions.py @@ -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 diff --git a/plinth/tests/functional/step_definitions/__init__.py b/plinth/tests/functional/step_definitions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plinth/tests/functional/step_definitions/application.py b/plinth/tests/functional/step_definitions/application.py deleted file mode 100644 index 5692b5f64..000000000 --- a/plinth/tests/functional/step_definitions/application.py +++ /dev/null @@ -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) diff --git a/plinth/tests/functional/step_definitions/interface.py b/plinth/tests/functional/step_definitions/interface.py deleted file mode 100644 index 512d7d953..000000000 --- a/plinth/tests/functional/step_definitions/interface.py +++ /dev/null @@ -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) diff --git a/plinth/tests/functional/step_definitions/service.py b/plinth/tests/functional/step_definitions/service.py deleted file mode 100644 index ec543d136..000000000 --- a/plinth/tests/functional/step_definitions/service.py +++ /dev/null @@ -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') diff --git a/plinth/tests/functional/step_definitions/site.py b/plinth/tests/functional/step_definitions/site.py deleted file mode 100644 index c5b23d6ba..000000000 --- a/plinth/tests/functional/step_definitions/site.py +++ /dev/null @@ -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) diff --git a/plinth/tests/functional/step_definitions/system.py b/plinth/tests/functional/step_definitions/system.py deleted file mode 100644 index 89a308c01..000000000 --- a/plinth/tests/functional/step_definitions/system.py +++ /dev/null @@ -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 ') -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 ') -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) diff --git a/plinth/tests/functional/support/__init__.py b/plinth/tests/functional/support/__init__.py deleted file mode 100644 index 9532249c2..000000000 --- a/plinth/tests/functional/support/__init__.py +++ /dev/null @@ -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']) diff --git a/plinth/tests/functional/support/application.py b/plinth/tests/functional/support/application.py deleted file mode 100644 index 9fbb1e2c4..000000000 --- a/plinth/tests/functional/support/application.py +++ /dev/null @@ -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 diff --git a/plinth/tests/functional/support/interface.py b/plinth/tests/functional/support/interface.py deleted file mode 100644 index 5e9f43339..000000000 --- a/plinth/tests/functional/support/interface.py +++ /dev/null @@ -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') diff --git a/plinth/tests/functional/support/service.py b/plinth/tests/functional/support/service.py deleted file mode 100644 index 02b5d83b8..000000000 --- a/plinth/tests/functional/support/service.py +++ /dev/null @@ -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 . - 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 diff --git a/plinth/tests/functional/support/site.py b/plinth/tests/functional/support/site.py deleted file mode 100644 index bd8ec80fc..000000000 --- a/plinth/tests/functional/support/site.py +++ /dev/null @@ -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') diff --git a/plinth/tests/functional/support/system.py b/plinth/tests/functional/support/system.py deleted file mode 100644 index bd8e3df97..000000000 --- a/plinth/tests/functional/support/system.py +++ /dev/null @@ -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)