diff --git a/HACKING.md b/HACKING.md
index 224bbcea9..d0ca146d1 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -171,6 +171,7 @@ $ pip3 install pytest-splinter
$ sudo apt install python3-pytest-bdd
$ sudo apt install xvfb python3-pytest-xvfb # optional, to avoid opening browser windows
$ sudo apt install firefox
+$ sudo apt install smbclient # optional, to test samba
```
- Install the latest version of geckodriver. It is usually a single binary which
@@ -187,6 +188,9 @@ The VM should have NAT port-forwarding enabled so that 4430 on the
host forwards to 443 on the guest. From where the tests are running, the web
interface of FreedomBox should be accessible at https://localhost:4430/.
+To run samba tests, port 4450 on the host should be forwarded to port 445
+on the guest.
+
### Setup FreedomBox Service for tests
Via Plinth, create a new user as follows:
@@ -202,7 +206,7 @@ tests will create the required user using FreedomBox's first boot process.
**When inside a VM you will need to target the guest VM**
```bash
-export FREEDOMBOX_URL=https://localhost
+export FREEDOMBOX_URL=https://localhost FREEDOMBOX_SAMBA_PORT=445
```
You will be running `py.test-3`.
diff --git a/Vagrantfile b/Vagrantfile
index 6e4ce82fa..37737a89e 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -19,6 +19,7 @@
Vagrant.configure(2) do |config|
config.vm.box = "freedombox/plinth-dev"
config.vm.network "forwarded_port", guest: 443, host: 4430
+ config.vm.network "forwarded_port", guest: 445, host: 4450
config.vm.synced_folder ".", "/vagrant", owner: "plinth", group: "plinth"
config.vm.provider "virtualbox" do |vb|
vb.cpus = 2
diff --git a/functional_tests/config.ini b/functional_tests/config.ini
index 77313c4e2..ba78055e2 100644
--- a/functional_tests/config.ini
+++ b/functional_tests/config.ini
@@ -3,3 +3,4 @@ url = https://localhost:4430
username = tester
password = testingtesting
delete_root_backup_archives = true
+samba_port = 4450
diff --git a/functional_tests/features/samba.feature b/functional_tests/features/samba.feature
index d1b500171..cbad4ae50 100644
--- a/functional_tests/features/samba.feature
+++ b/functional_tests/features/samba.feature
@@ -15,12 +15,13 @@
# along with this program. If not, see .
#
-@apps @samba
+@apps @samba @backups
Feature: Samba File Sharing
Configure samba file sharing service.
Background:
Given I'm a logged in user
+ Given the network device is in the internal firewall zone
Given the samba application is installed
Scenario: Enable samba application
@@ -33,3 +34,34 @@ Scenario: Disable samba application
When I disable the samba application
Then the samba service should not be running
+Scenario: Enable open samba share
+ Given the samba application is enabled
+ When I enable the open samba share
+ Then I can write to the open samba share
+ And a guest user can write to the open samba share
+
+Scenario: Enable group samba share
+ Given the samba application is enabled
+ When I enable the group samba share
+ Then I can write to the group samba share
+ And a guest user can't access the group samba share
+
+Scenario: Enable home samba share
+ Given the samba application is enabled
+ When I enable the home samba share
+ Then I can write to the home samba share
+ And a guest user can't access the home samba share
+
+Scenario: Disable open samba share
+ Given the samba application is enabled
+ When I disable the open samba share
+ Then the open samba share should not be available
+
+Scenario: Backup and restore samba
+ Given the samba application is enabled
+ When I enable the home samba share
+ And I create a backup of the samba app data
+ And I disable the home samba share
+ And I restore the samba app data backup
+ Then the samba service should be running
+ And I can write to the home samba share
diff --git a/functional_tests/install.sh b/functional_tests/install.sh
index 7c5e1f563..68675fe98 100755
--- a/functional_tests/install.sh
+++ b/functional_tests/install.sh
@@ -5,7 +5,7 @@ IFS=$'\n\t'
echo "Installing requirements"
sudo apt-get install -yq --no-install-recommends \
python3-pytest python3-pytest-django \
- python3-pip firefox \
+ python3-pip firefox smbclient\
xvfb
pip3 install wheel
pip3 install splinter pytest-splinter pytest-bdd pytest-xvfb
diff --git a/functional_tests/step_definitions/application.py b/functional_tests/step_definitions/application.py
index 22db2c979..293057173 100644
--- a/functional_tests/step_definitions/application.py
+++ b/functional_tests/step_definitions/application.py
@@ -602,3 +602,31 @@ 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(browser, task, share_type):
+ if task == 'enable':
+ application.samba_set_share(browser, share_type, status='enabled')
+ elif task == 'disable':
+ application.samba_set_share(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/functional_tests/step_definitions/system.py b/functional_tests/step_definitions/system.py
index 51bc823c0..687a706dd 100644
--- a/functional_tests/step_definitions/system.py
+++ b/functional_tests/step_definitions/system.py
@@ -347,3 +347,8 @@ def open_main_page(browser):
@then(parsers.parse('the main page should be shown'))
def main_page_is_shown(browser):
assert (browser.url.endswith('/plinth/'))
+
+
+@given(parsers.parse('the network device is in the {zone:w} firewall zone'))
+def networks_set_firewall_zone(browser, zone):
+ system.networks_set_firewall_zone(browser, zone)
diff --git a/functional_tests/support/__init__.py b/functional_tests/support/__init__.py
index 93300ac79..7b8b633d5 100644
--- a/functional_tests/support/__init__.py
+++ b/functional_tests/support/__init__.py
@@ -24,3 +24,5 @@ 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/functional_tests/support/application.py b/functional_tests/support/application.py
index 8086bcd58..da01d020d 100644
--- a/functional_tests/support/application.py
+++ b/functional_tests/support/application.py
@@ -17,9 +17,12 @@
import contextlib
import os
+import random
import shutil
+import string
import subprocess
import tempfile
+import urllib
from time import sleep
import requests
@@ -664,6 +667,79 @@ def openvpn_download_profile(browser):
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')
diff --git a/functional_tests/support/system.py b/functional_tests/support/system.py
index b8be64996..431f069f9 100644
--- a/functional_tests/support/system.py
+++ b/functional_tests/support/system.py
@@ -417,3 +417,16 @@ def monkeysphere_publish_key(browser, key_type, domain):
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()
diff --git a/plinth/modules/samba/tests/test_views.py b/plinth/modules/samba/tests/test_views.py
new file mode 100644
index 000000000..cf83e09a8
--- /dev/null
+++ b/plinth/modules/samba/tests/test_views.py
@@ -0,0 +1,187 @@
+#
+# 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 .
+#
+"""
+Tests for samba views.
+"""
+
+import json
+import urllib
+from unittest.mock import patch
+
+import pytest
+from django import urls
+from django.contrib.messages.storage.fallback import FallbackStorage
+from plinth import module_loader
+from plinth.errors import ActionError
+from plinth.modules.samba import views
+
+# For all tests, use plinth.urls instead of urls configured for testing
+pytestmark = pytest.mark.urls('plinth.urls')
+
+USERS = {"access_ok": ["testuser"], 'password_re_enter_needed': []}
+
+DISKS = [{
+ 'device': '/dev/sda1',
+ 'label': '',
+ 'filesystem_type': 'ext4',
+ 'mount_point': '/',
+ 'file_system_type': 'ext4',
+ 'percent_used': 63,
+ 'size_str': '9.5 GiB',
+ 'used_str': '5.7 GiB'
+}]
+
+SHARES = [
+ {
+ "name": "disk",
+ "mount_point": "/",
+ "path": "/var/lib/freedombox/shares/open_share",
+ "share_type": "open"
+ },
+ {
+ "name": "disk_home",
+ "mount_point": "/",
+ "path": "/var/lib/freedombox/shares/homes/%u",
+ "share_type": "home"
+ },
+ {
+ "name": "otherdisk",
+ "mount_point": "/media/root/otherdisk",
+ "path": "/media/root/otherdisk/FreedomBox/shares/homes/open_share",
+ "share_type": "open"
+ }
+]
+
+
+@pytest.fixture(autouse=True, scope='module')
+def fixture_samba_urls():
+ """Make sure samba app's URLs are part of plinth.urls."""
+ with patch('plinth.module_loader._modules_to_load', new=[]) as modules, \
+ patch('plinth.urls.urlpatterns', new=[]):
+ modules.append('plinth.modules.samba')
+ module_loader.include_urls()
+ yield
+
+
+def action_run(action, options, **kwargs):
+ """Action return values."""
+ if action == 'samba' and options == ['get-shares']:
+ return json.dumps(SHARES)
+
+ return None
+
+
+@pytest.fixture(autouse=True)
+def samba_patch_actions():
+ """Patch actions scripts runner."""
+ with patch('plinth.actions.superuser_run', side_effect=action_run):
+ yield
+
+
+def make_request(request, view, **kwargs):
+ """Make request with a message storage."""
+ setattr(request, 'session', 'session')
+ messages = FallbackStorage(request)
+ setattr(request, '_messages', messages)
+ response = view(request, **kwargs)
+
+ return response, messages
+
+
+def test_samba_shares_view(rf):
+ """Test that a share list has correct view data."""
+ with patch('plinth.views.AppView.get_context_data',
+ return_value={'is_enabled': True}), patch(
+ 'plinth.modules.samba.get_users',
+ return_value=USERS), patch(
+ 'plinth.modules.storage.get_disks', return_value=DISKS):
+ view = views.SambaAppView.as_view()
+ response, _ = make_request(rf.get(''), view)
+
+ assert response.context_data['disks'] == DISKS
+ assert response.context_data['shared_mounts'] == {
+ '/': ['open', 'home'],
+ '/media/root/otherdisk': ['open']
+ }
+ assert response.context_data['unavailable_shares'] == [{
+ 'mount_point':
+ '/media/root/otherdisk',
+ 'name':
+ 'otherdisk',
+ 'path':
+ '/media/root/otherdisk/FreedomBox/shares/homes/open_share',
+ 'share_type':
+ 'open'
+ }]
+ assert response.context_data['users'] == USERS
+ assert response.status_code == 200
+
+
+def test_enable_samba_share_view(rf):
+ """Test that enabling share sends correct success message."""
+ form_data = {'filesystem_type': 'ext4', 'open_share': 'enable'}
+ mount_point = urllib.parse.quote('/')
+ response, messages = make_request(
+ rf.post('', data=form_data), views.share, mount_point=mount_point)
+
+ assert list(messages)[0].message == 'Share enabled.'
+ assert response.status_code == 302
+ assert response.url == urls.reverse('samba:index')
+
+
+def test_enable_samba_share_failed_view(rf):
+ """Test that share enabling failure sends correct error message."""
+ form_data = {'filesystem_type': 'ext4', 'open_share': 'enable'}
+ mount_point = urllib.parse.quote('/')
+ error_message = 'Sharing failed'
+ with patch('plinth.modules.samba.add_share',
+ side_effect=ActionError(error_message)):
+ response, messages = make_request(
+ rf.post('', data=form_data), views.share, mount_point=mount_point)
+
+ assert list(messages)[0].message == 'Error enabling share: {0}'.format(
+ error_message)
+ assert response.status_code == 302
+ assert response.url == urls.reverse('samba:index')
+
+
+def test_disable_samba_share(rf):
+ """Test that enabling share sends correct success message."""
+ form_data = {'filesystem_type': 'ext4', 'open_share': 'disable'}
+ mount_point = urllib.parse.quote('/')
+ response, messages = make_request(
+ rf.post('', data=form_data), views.share, mount_point=mount_point)
+
+ assert list(messages)[0].message == 'Share disabled.'
+ assert response.status_code == 302
+ assert response.url == urls.reverse('samba:index')
+
+
+def test_disable_samba_share_failed_view(rf):
+ """Test that share disabling failure sends correct error message."""
+ form_data = {'filesystem_type': 'ext4', 'open_share': 'disable'}
+ mount_point = urllib.parse.quote('/')
+ error_message = 'Unsharing failed'
+ with patch('plinth.modules.samba.delete_share',
+ side_effect=ActionError(error_message)):
+ response, messages = make_request(
+ rf.post('', data=form_data), views.share, mount_point=mount_point)
+
+ assert list(messages)[
+ 0].message == 'Error disabling share: {0}'.format(error_message)
+ assert response.status_code == 302
+ assert response.url == urls.reverse('samba:index')