From 3a202af843b420dd3743ff547e1aaaeeb72b0e38 Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Thu, 16 Jan 2020 19:23:25 +0200 Subject: [PATCH] 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 Reviewed-by: Joseph Nuthalapati Signed-off-by: Joseph Nuthalapati --- HACKING.md | 6 +- Vagrantfile | 1 + functional_tests/config.ini | 1 + functional_tests/features/samba.feature | 34 +++- functional_tests/install.sh | 2 +- .../step_definitions/application.py | 28 +++ functional_tests/step_definitions/system.py | 5 + functional_tests/support/__init__.py | 2 + functional_tests/support/application.py | 76 +++++++ functional_tests/support/system.py | 13 ++ plinth/modules/samba/tests/test_views.py | 187 ++++++++++++++++++ 11 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 plinth/modules/samba/tests/test_views.py 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')