diff --git a/functional_tests/features/openvpn.feature b/functional_tests/features/openvpn.feature
new file mode 100644
index 000000000..140be4fd8
--- /dev/null
+++ b/functional_tests/features/openvpn.feature
@@ -0,0 +1,46 @@
+#
+# 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 @openvpn @backups
+Feature: OpenVPN - Virtual Private Network
+ Setup and configure OpenVPN
+
+Background:
+ Given I'm a logged in user
+ Given the openvpn application is installed
+ Given the openvpn application is setup
+
+Scenario: Enable openvpn application
+ Given the openvpn application is disabled
+ When I enable the openvpn application
+ Then the openvpn service should be running
+
+Scenario: Download openvpn profile
+ Given the openvpn application is enabled
+ Then the openvpn profile should be downloadable
+
+Scenario: Backup and restore openvpn
+ Given the openvpn application is enabled
+ And I download openvpn profile
+ When I create a backup of the openvpn app data
+ And I restore the openvpn app data backup
+ Then the openvpn profile downloaded should be same as before
+
+Scenario: Disable openvpn application
+ Given the openvpn application is enabled
+ When I disable the openvpn application
+ Then the openvpn service should not be running
diff --git a/functional_tests/step_definitions/application.py b/functional_tests/step_definitions/application.py
index 7fdf7f68a..e1e517ff9 100644
--- a/functional_tests/step_definitions/application.py
+++ b/functional_tests/step_definitions/application.py
@@ -25,7 +25,7 @@ from support import application
@given(parsers.parse('the {app_name:w} application is installed'))
def application_is_installed(browser, app_name):
application.install(browser, app_name)
- assert(application.is_installed(browser, app_name))
+ assert (application.is_installed(browser, app_name))
@given(parsers.parse('the {app_name:w} application is enabled'))
@@ -331,14 +331,19 @@ def tor_assert_download_software_over_tor(browser, enabled):
application.tor_assert_feature_enabled(browser, 'software', enabled)
-@then(parsers.parse('{domain:S} should be a tahoe {introducer_type:w} introducer'))
+@then(
+ parsers.parse(
+ '{domain:S} should be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_introducer(browser, domain, introducer_type):
assert application.tahoe_get_introducer(browser, domain, introducer_type)
-@then(parsers.parse('{domain:S} should not be a tahoe {introducer_type:w} introducer'))
+@then(
+ parsers.parse(
+ '{domain:S} should not be a tahoe {introducer_type:w} introducer'))
def tahoe_assert_not_introducer(browser, domain, introducer_type):
- assert not application.tahoe_get_introducer(browser, domain, introducer_type)
+ assert not application.tahoe_get_introducer(browser, domain,
+ introducer_type)
@given(parsers.parse('{domain:S} is not a tahoe introducer'))
@@ -397,3 +402,24 @@ def radicale_check_owner_write(browser):
@then('the access rights should be "any user can view or make changes"')
def radicale_check_authenticated(browser):
assert application.radicale_get_access_rights(browser) == 'authenticated'
+
+
+@given(parsers.parse('the openvpn application is setup'))
+def openvpn_setup(browser):
+ application.openvpn_setup(browser)
+
+
+@given('I download openvpn profile')
+def openvpn_download_profile(browser):
+ return application.openvpn_download_profile(browser)
+
+
+@then('the openvpn profile should be downloadable')
+def openvpn_profile_downloadable(browser):
+ application.openvpn_download_profile(browser)
+
+
+@then('the openvpn profile downloaded should be same as before')
+def openvpn_profile_download_compare(browser, openvpn_download_profile):
+ new_profile = application.openvpn_download_profile(browser)
+ assert openvpn_download_profile == new_profile
diff --git a/functional_tests/support/application.py b/functional_tests/support/application.py
index 03e556321..72bf7a34a 100644
--- a/functional_tests/support/application.py
+++ b/functional_tests/support/application.py
@@ -17,6 +17,7 @@
from time import sleep
+import requests
import splinter
from support import config, interface, site
@@ -32,6 +33,7 @@ app_module = {
app_checkbox_id = {
'tor': 'id_tor-enabled',
+ 'openvpn': 'id_openvpn-enabled',
}
default_url = config['DEFAULT']['url']
@@ -115,6 +117,16 @@ def wait_for_config_update(browser, app_name):
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')
@@ -439,3 +451,21 @@ 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)
diff --git a/plinth/modules/openvpn/__init__.py b/plinth/modules/openvpn/__init__.py
index dce31e9e3..ebfae2e92 100644
--- a/plinth/modules/openvpn/__init__.py
+++ b/plinth/modules/openvpn/__init__.py
@@ -26,6 +26,8 @@ from plinth import action_utils, actions, cfg, frontpage
from plinth.menu import main_menu
from plinth.utils import format_lazy
+from .manifest import backup
+
version = 2
service = None
diff --git a/plinth/modules/openvpn/manifest.py b/plinth/modules/openvpn/manifest.py
new file mode 100644
index 000000000..05b241959
--- /dev/null
+++ b/plinth/modules/openvpn/manifest.py
@@ -0,0 +1,28 @@
+#
+# 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 .
+#
+"""
+Application manifest for OpenVPN.
+"""
+
+from plinth.modules.backups.api import validate as validate_backup
+
+
+backup = validate_backup({
+ 'secrets': {
+ 'directories': ['/etc/openvpn/']
+ }
+})
diff --git a/plinth/modules/openvpn/templates/openvpn.html b/plinth/modules/openvpn/templates/openvpn.html
index 6ac6d0c90..6c24c9a4a 100644
--- a/plinth/modules/openvpn/templates/openvpn.html
+++ b/plinth/modules/openvpn/templates/openvpn.html
@@ -58,7 +58,8 @@
{% endblocktrans %}
-