samba: Add unit and functional tests

- Add functional tests
- Add unit tests for samba views
- New dependency for running functional tests: smbclient
- Make port configurable for the smbclient

Signed-off-by: Veiko Aasa <veiko17@disroot.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
Veiko Aasa 2020-01-16 19:23:25 +02:00 committed by Joseph Nuthalapati
parent aa69585cb6
commit 3a202af843
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
11 changed files with 352 additions and 3 deletions

View File

@ -171,6 +171,7 @@ $ pip3 install pytest-splinter
$ sudo apt install python3-pytest-bdd $ sudo apt install python3-pytest-bdd
$ sudo apt install xvfb python3-pytest-xvfb # optional, to avoid opening browser windows $ sudo apt install xvfb python3-pytest-xvfb # optional, to avoid opening browser windows
$ sudo apt install firefox $ 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 - 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 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/. 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 ### Setup FreedomBox Service for tests
Via Plinth, create a new user as follows: 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** **When inside a VM you will need to target the guest VM**
```bash ```bash
export FREEDOMBOX_URL=https://localhost export FREEDOMBOX_URL=https://localhost FREEDOMBOX_SAMBA_PORT=445
``` ```
You will be running `py.test-3`. You will be running `py.test-3`.

1
Vagrantfile vendored
View File

@ -19,6 +19,7 @@
Vagrant.configure(2) do |config| Vagrant.configure(2) do |config|
config.vm.box = "freedombox/plinth-dev" config.vm.box = "freedombox/plinth-dev"
config.vm.network "forwarded_port", guest: 443, host: 4430 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.synced_folder ".", "/vagrant", owner: "plinth", group: "plinth"
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
vb.cpus = 2 vb.cpus = 2

View File

@ -3,3 +3,4 @@ url = https://localhost:4430
username = tester username = tester
password = testingtesting password = testingtesting
delete_root_backup_archives = true delete_root_backup_archives = true
samba_port = 4450

View File

@ -15,12 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
@apps @samba @apps @samba @backups
Feature: Samba File Sharing Feature: Samba File Sharing
Configure samba file sharing service. Configure samba file sharing service.
Background: Background:
Given I'm a logged in user Given I'm a logged in user
Given the network device is in the internal firewall zone
Given the samba application is installed Given the samba application is installed
Scenario: Enable samba application Scenario: Enable samba application
@ -33,3 +34,34 @@ Scenario: Disable samba application
When I disable the samba application When I disable the samba application
Then the samba service should not be running 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

View File

@ -5,7 +5,7 @@ IFS=$'\n\t'
echo "Installing requirements" echo "Installing requirements"
sudo apt-get install -yq --no-install-recommends \ sudo apt-get install -yq --no-install-recommends \
python3-pytest python3-pytest-django \ python3-pytest python3-pytest-django \
python3-pip firefox \ python3-pip firefox smbclient\
xvfb xvfb
pip3 install wheel pip3 install wheel
pip3 install splinter pytest-splinter pytest-bdd pytest-xvfb pip3 install splinter pytest-splinter pytest-bdd pytest-xvfb

View File

@ -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)
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) 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)

View File

@ -347,3 +347,8 @@ def open_main_page(browser):
@then(parsers.parse('the main page should be shown')) @then(parsers.parse('the main page should be shown'))
def main_page_is_shown(browser): def main_page_is_shown(browser):
assert (browser.url.endswith('/plinth/')) 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)

View File

@ -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'] = os.environ.get('FREEDOMBOX_URL',
config['DEFAULT']['url']) config['DEFAULT']['url'])
config['DEFAULT']['samba_port'] = os.environ.get('FREEDOMBOX_SAMBA_PORT',
config['DEFAULT']['samba_port'])

View File

@ -17,9 +17,12 @@
import contextlib import contextlib
import os import os
import random
import shutil import shutil
import string
import subprocess import subprocess
import tempfile import tempfile
import urllib
from time import sleep from time import sleep
import requests import requests
@ -664,6 +667,79 @@ def openvpn_download_profile(browser):
return _download_file(browser, url) 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): def searx_enable_public_access(browser):
"""Enable Public Access in SearX""" """Enable Public Access in SearX"""
interface.nav_to_module(browser, 'searx') interface.nav_to_module(browser, 'searx')

View File

@ -417,3 +417,16 @@ def monkeysphere_publish_key(browser, key_type, domain):
def open_main_page(browser): def open_main_page(browser):
with wait_for_page_update(browser): with wait_for_page_update(browser):
browser.find_link_by_href('/plinth/').first.click() 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()

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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')