diff --git a/functional_tests/features/syncthing.feature b/functional_tests/features/syncthing.feature index 27bffb1df..0c94a9fd5 100644 --- a/functional_tests/features/syncthing.feature +++ b/functional_tests/features/syncthing.feature @@ -15,7 +15,7 @@ # along with this program. If not, see . # -@apps @syncthing @sso +@apps @syncthing @sso @backups Feature: Syncthing File Synchronization Run Syncthing File Synchronization server. @@ -28,6 +28,27 @@ Scenario: Enable syncthing application When I enable the syncthing application Then the syncthing service should be running +Scenario: Add a syncthing folder + Given the syncthing application is enabled + And syncthing folder Test is not present + When I add a folder /tmp as syncthing folder Test + Then syncthing folder Test should be present + +Scenario: Remove a syncthing folder + Given the syncthing application is enabled + And folder /tmp is present as syncthing folder Test + When I remove syncthing folder Test + Then syncthing folder Test should not be present + +Scenario: Backup and restore syncthing + Given the syncthing application is enabled + And syncthing folder Test is not present + When I add a folder /tmp as syncthing folder Test + And I create a backup of the syncthing app data + And I remove syncthing folder Test + And I restore the syncthing app data backup + Then syncthing folder Test should be present + Scenario: Disable syncthing application Given the syncthing application is enabled When I disable the syncthing application diff --git a/functional_tests/step_definitions/site.py b/functional_tests/step_definitions/site.py index 6c02a43b1..583f3fb2f 100644 --- a/functional_tests/step_definitions/site.py +++ b/functional_tests/step_definitions/site.py @@ -147,8 +147,7 @@ def transmission_upload_sample_torrent(browser): @then( parsers.parse( - 'there should be {torrents_number:d} torrents listed in transmission' - )) + 'there should be {torrents_number:d} torrents listed in transmission')) def transmission_assert_number_of_torrents(browser, torrents_number): assert torrents_number == site.transmission_get_number_of_torrents(browser) @@ -165,8 +164,7 @@ def deluge_upload_sample_torrent(browser): @then( parsers.parse( - 'there should be {torrents_number:d} torrents listed in deluge' - )) + 'there should be {torrents_number:d} torrents listed in deluge')) def deluge_assert_number_of_torrents(browser, torrents_number): assert torrents_number == site.deluge_get_number_of_torrents(browser) @@ -189,3 +187,39 @@ def assert_addressbook_is_available(browser): @then('the addressbook should not be available') def assert_addressbook_is_not_available(browser): assert not site.addressbook_is_available(browser) + + +@given(parsers.parse('syncthing folder {folder_name:w} is not present')) +def syncthing_folder_not_present(browser, folder_name): + if site.syncthing_folder_is_present(browser, folder_name): + site.syncthing_remove_folder(browser, folder_name) + + +@given( + parsers.parse( + 'folder {folder_path:S} is present as syncthing folder {folder_name:w}')) +def syncthing_folder_present(browser, folder_name, folder_path): + if not site.syncthing_folder_is_present(browser, folder_name): + site.syncthing_add_folder(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(browser, folder_name, folder_path): + site.syncthing_add_folder(browser, folder_name, folder_path) + + +@when(parsers.parse('I remove syncthing folder {folder_name:w}')) +def syncthing_remove_folder(browser, folder_name): + site.syncthing_remove_folder(browser, folder_name) + + +@then(parsers.parse('syncthing folder {folder_name:w} should be present')) +def syncthing_assert_folder_present(browser, folder_name): + assert site.syncthing_folder_is_present(browser, folder_name) + + +@then(parsers.parse('syncthing folder {folder_name:w} should not be present')) +def syncthing_assert_folder_not_present(browser, folder_name): + assert not site.syncthing_folder_is_present(browser, folder_name) diff --git a/functional_tests/support/site.py b/functional_tests/support/site.py index 1cdbc9f21..ef34f4174 100644 --- a/functional_tests/support/site.py +++ b/functional_tests/support/site.py @@ -31,7 +31,8 @@ from support.service import eventually, wait_for_page_update site_url = { 'wiki': '/ikiwiki', 'jsxc': '/plinth/apps/jsxc/jsxc/', - 'cockpit': '/_cockpit/' + 'cockpit': '/_cockpit/', + 'syncthing': '/syncthing/', } @@ -432,8 +433,8 @@ def calendar_is_available(browser): conf = config['DEFAULT'] url = conf['url'] + '/.well-known/caldav' logging.captureWarnings(True) - request = requests.get( - url, auth=(conf['username'], conf['password']), verify=False) + request = requests.get(url, auth=(conf['username'], conf['password']), + verify=False) logging.captureWarnings(False) return request.status_code != 404 @@ -443,7 +444,103 @@ def addressbook_is_available(browser): conf = config['DEFAULT'] url = conf['url'] + '/.well-known/carddav' logging.captureWarnings(True) - request = requests.get( - url, auth=(conf['username'], conf['password']), verify=False) + 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) diff --git a/plinth/modules/syncthing/__init__.py b/plinth/modules/syncthing/__init__.py index 10741ffbc..7a60fc169 100644 --- a/plinth/modules/syncthing/__init__.py +++ b/plinth/modules/syncthing/__init__.py @@ -26,11 +26,11 @@ from plinth.menu import main_menu from plinth.modules.users import register_group from plinth.utils import format_lazy -from .manifest import clients +from .manifest import backup, clients version = 1 -managed_services = ['syncthing'] +managed_services = ['syncthing@syncthing'] managed_packages = ['syncthing'] diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py index 3a9f0352b..2784ee68e 100644 --- a/plinth/modules/syncthing/manifest.py +++ b/plinth/modules/syncthing/manifest.py @@ -18,6 +18,7 @@ from django.utils.translation import ugettext_lazy as _ from plinth.clients import store_url, validate +from plinth.modules.backups.api import validate as validate_backup _package_id = 'com.nutomic.syncthingandroid' _download_url = 'https://syncthing.net/' @@ -60,3 +61,10 @@ clients = validate([{ 'url': '/syncthing' }] }]) + +backup = validate_backup({ + 'secrets': { + 'directories': ['/var/lib/syncthing/.config'] + }, + 'services': ['syncthing@syncthing'] +})