diff --git a/functional_tests/features/sharing.feature b/functional_tests/features/sharing.feature
new file mode 100644
index 000000000..44c93006f
--- /dev/null
+++ b/functional_tests/features/sharing.feature
@@ -0,0 +1,52 @@
+#
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+@apps @sharing
+Feature: Sharing
+ Share server folders over HTTP, etc.
+
+Background:
+ Given I'm a logged in user
+
+Scenario: Add new share
+ Given share tmp is not available
+ When I add a share tmp from path /tmp for admin
+ Then the share tmp should be listed from path /tmp for admin
+ Then the share tmp should be accessible
+
+Scenario: Edit a share
+ Given share tmp is not available
+ When I remove share boot
+ When I add a share tmp from path /tmp for admin
+ When I edit share tmp to boot from path /boot for admin
+ Then the share tmp should not be listed
+ Then the share tmp should not exist
+ Then the share boot should be listed from path /boot for admin
+ Then the share boot should be accessible
+
+Scenario: Remove a share
+ When I remove share tmp
+ When I add a share tmp from path /tmp for admin
+ When I remove share tmp
+ Then the share tmp should not be listed
+ Then the share tmp should not exist
+
+Scenario: Share permissions
+ When I remove share tmp
+ When I add a share tmp from path /tmp for syncthing
+ Then the share tmp should be listed from path /tmp for syncthing
+ Then the share tmp should not be accessible
diff --git a/functional_tests/step_definitions/application.py b/functional_tests/step_definitions/application.py
index af2650622..d7ff7c7ed 100644
--- a/functional_tests/step_definitions/application.py
+++ b/functional_tests/step_definitions/application.py
@@ -15,6 +15,8 @@
# along with this program. If not, see .
#
+import pytest
+import splinter
from pytest_bdd import given, parsers, then, when
from support import application
@@ -111,3 +113,54 @@ def assert_max_file_size(browser, size):
@when(parsers.parse('I modify the coquelicot upload password to {password:w}'))
def modify_upload_password(browser, password):
application.modify_upload_password(browser, password)
+
+
+@given(parsers.parse('share {name:w} is not available'))
+def remove_share(browser, name):
+ application.remove_share(browser, name)
+
+
+@when(parsers.parse('I add a share {name:w} from path {path} for {group:w}'))
+def add_share(browser, name, path, group):
+ application.add_share(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(browser, old_name, new_name, path, group):
+ application.edit_share(browser, old_name, new_name, path, group)
+
+
+@when(parsers.parse('I remove share {name:w}'))
+def remove_share2(browser, name):
+ application.remove_share(browser, name)
+
+
+@then(
+ parsers.parse(
+ 'the share {name:w} should be listed from path {path} for {group:w}'))
+def verify_share(browser, name, path, group):
+ application.verify_share(browser, name, path, group)
+
+
+@then(parsers.parse('the share {name:w} should not be listed'))
+def verify_invalid_share(browser, name):
+ with pytest.raises(splinter.exceptions.ElementDoesNotExist):
+ application.get_share(browser, name)
+
+
+@then(parsers.parse('the share {name:w} should be accessible'))
+def access_share(browser, name):
+ application.access_share(browser, name)
+
+
+@then(parsers.parse('the share {name:w} should not exist'))
+def verify_nonexistant_share(browser, name):
+ application.verify_nonexistant_share(browser, name)
+
+
+@then(parsers.parse('the share {name:w} should not be accessible'))
+def verify_inaccessible_share(browser, name):
+ application.verify_inaccessible_share(browser, name)
diff --git a/functional_tests/support/application.py b/functional_tests/support/application.py
index 5e06385f8..a0ea35703 100644
--- a/functional_tests/support/application.py
+++ b/functional_tests/support/application.py
@@ -17,6 +17,7 @@
from time import sleep
+import splinter
from support import config, interface
from support.service import eventually
@@ -128,3 +129,77 @@ def modify_upload_password(browser, password):
browser.find_by_value('Update setup').click()
# Wait for the service to restart after updating password
eventually(browser.is_text_present, args=['Upload password updated'])
+
+
+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()
+ browser.find_by_css('input[type="submit"]').click()
+ eventually(browser.is_text_present, args=['Share added.'])
+
+
+def edit_share(browser, old_name, new_name, path, group):
+ """Edit a share in sharing app."""
+ row = get_share(browser, old_name)
+ row.find_by_css('.share-edit')[0].click()
+ eventually(browser.is_text_present, args=['Edit Share'])
+ 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()
+ browser.find_by_css('input[type="submit"]').click()
+ eventually(browser.is_text_present, args=['Share edited.'])
+
+
+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)
+ browser.is_text_present('Index of /share/{}'.format(name))
+
+
+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)
+ browser.is_text_present('Not Found')
+
+
+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=[])