wrolpi/controller/test/test_samba.py
Roland 1e2e59ec2e
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
Adding docker support for samba shares.
2026-03-29 11:14:38 -06:00

322 lines
16 KiB
Python

"""
Unit tests for controller.lib.samba module.
"""
import subprocess
from unittest import mock
from controller.lib.samba import (
SambaStatus,
_generate_smb_conf,
_get_smb_conf_path,
_reload_samba,
_start_samba,
_stop_samba,
add_share,
apply_samba_from_config,
get_samba_status,
get_samba_status_dict,
remove_share,
)
class TestGetSambaStatus:
"""Tests for get_samba_status function."""
def test_returns_running_when_active(self):
mock_result = mock.Mock()
mock_result.stdout = "active\n"
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run", return_value=mock_result):
assert get_samba_status() == SambaStatus.running
def test_returns_stopped_when_inactive(self):
mock_result = mock.Mock()
mock_result.stdout = "inactive\n"
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run", return_value=mock_result):
assert get_samba_status() == SambaStatus.stopped
def test_returns_unavailable_when_systemctl_not_found(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run", side_effect=FileNotFoundError()):
assert get_samba_status() == SambaStatus.unavailable
def test_returns_unknown_on_timeout(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 5)):
assert get_samba_status() == SambaStatus.unknown
def test_docker_mode_returns_running(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=True):
with mock.patch("controller.lib.docker_services.get_container_status",
return_value={"status": "running"}):
assert get_samba_status() == SambaStatus.running
def test_docker_mode_returns_stopped(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=True):
with mock.patch("controller.lib.docker_services.get_container_status",
return_value={"status": "stopped"}):
assert get_samba_status() == SambaStatus.stopped
def test_docker_mode_unavailable_without_docker(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=False):
assert get_samba_status() == SambaStatus.unavailable
class TestGetSambaStatusDict:
"""Tests for get_samba_status_dict function."""
def test_returns_expected_fields(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=False):
result = get_samba_status_dict()
assert "enabled" in result
assert "available" in result
assert "shares" in result
def test_unavailable_without_docker(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=False):
result = get_samba_status_dict()
assert result["available"] is False
assert result["reason"] is not None
class TestGetSmbConfPath:
"""Tests for _get_smb_conf_path function."""
def test_native_mode(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
assert str(_get_smb_conf_path()) == "/etc/samba/smb.conf"
def test_docker_mode(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
path = _get_smb_conf_path()
assert "config/smb.conf" in str(path)
class TestGenerateSmbConf:
"""Tests for _generate_smb_conf function."""
def test_generates_global_section(self, reset_runtime_config):
conf = _generate_smb_conf()
assert "[global]" in conf
assert "workgroup = WORKGROUP" in conf
assert "map to guest = Bad User" in conf
def test_generates_share_sections(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "TestShare", "path": "/media/wrolpi/test", "read_only": True, "comment": "Test"},
])
conf = _generate_smb_conf()
assert "[TestShare]" in conf
assert "path = /media/wrolpi/test" in conf
assert "read only = yes" in conf
assert "guest ok = yes" in conf
def test_read_write_share(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "Writable", "path": "/media/wrolpi", "read_only": False, "comment": ""},
])
conf = _generate_smb_conf()
assert "read only = no" in conf
def test_empty_shares_produces_global_only(self, reset_runtime_config):
conf = _generate_smb_conf()
assert "[global]" in conf
lines = conf.split("\n")
sections = [l for l in lines if l.startswith("[") and l != "[global]"]
assert len(sections) == 0
class TestReloadSamba:
"""Tests for _reload_samba function."""
def test_native_mode_calls_smbcontrol(self):
mock_result = mock.Mock()
mock_result.stdout = "active\n"
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run", return_value=mock_result) as mock_run:
_reload_samba()
calls = [str(c) for c in mock_run.call_args_list]
assert any("smbcontrol" in c for c in calls)
def test_docker_mode_restarts_container(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=True):
with mock.patch("controller.lib.docker_services.restart_container") as mock_restart:
_reload_samba()
mock_restart.assert_called_once_with("samba")
class TestStartStopSamba:
"""Tests for _start_samba and _stop_samba functions."""
def test_start_native(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run") as mock_run:
_start_samba()
calls = [str(c) for c in mock_run.call_args_list]
assert any("start" in c and "smbd" in c for c in calls)
def test_stop_native(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("subprocess.run") as mock_run:
_stop_samba()
calls = [str(c) for c in mock_run.call_args_list]
assert any("stop" in c and "smbd" in c for c in calls)
def test_start_docker(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=True):
with mock.patch("controller.lib.docker_services.start_container") as mock_start:
_start_samba()
mock_start.assert_called_once_with("samba")
def test_stop_docker(self):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=True):
with mock.patch("controller.lib.docker_services.can_manage_containers", return_value=True):
with mock.patch("controller.lib.docker_services.stop_container") as mock_stop:
_stop_samba()
mock_stop.assert_called_once_with("samba")
class TestAddShare:
"""Tests for add_share function."""
def test_rejects_invalid_name(self, reset_runtime_config):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
result = add_share("test/bad", "/media/wrolpi")
assert result["success"] is False
assert "alphanumeric" in result["error"]
def test_rejects_path_outside_media_dir(self, reset_runtime_config, test_directory):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("controller.lib.samba.get_media_directory", return_value=test_directory):
result = add_share("test", "/tmp")
assert result["success"] is False
assert "must be under" in result["error"]
def test_rejects_nonexistent_path(self, reset_runtime_config, test_directory):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("controller.lib.samba.get_media_directory", return_value=test_directory):
result = add_share("test", str(test_directory / "nonexistent"))
assert result["success"] is False
assert "does not exist" in result["error"]
def test_rejects_duplicate_name(self, reset_runtime_config, test_directory):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "existing", "path": str(test_directory), "read_only": True, "comment": ""},
])
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("controller.lib.samba.get_media_directory", return_value=test_directory):
with mock.patch("controller.lib.samba.save_config"):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
result = add_share("Existing", str(test_directory))
assert result["success"] is False
assert "already exists" in result["error"]
def test_adds_share_and_starts_samba(self, reset_runtime_config, test_directory):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("controller.lib.samba.get_media_directory", return_value=test_directory):
with mock.patch("controller.lib.samba.save_config"):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.stopped):
with mock.patch("controller.lib.samba._start_samba") as mock_start:
result = add_share("MyShare", str(test_directory))
assert result["success"] is True
mock_start.assert_called_once()
def test_adds_share_reloads_if_already_running(self, reset_runtime_config, test_directory):
with mock.patch("controller.lib.samba.is_docker_mode", return_value=False):
with mock.patch("controller.lib.samba.get_media_directory", return_value=test_directory):
with mock.patch("controller.lib.samba.save_config"):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.running):
with mock.patch("controller.lib.samba._reload_samba") as mock_reload:
result = add_share("MyShare", str(test_directory))
assert result["success"] is True
mock_reload.assert_called_once()
class TestRemoveShare:
"""Tests for remove_share function."""
def test_removes_share(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "test", "path": "/media/wrolpi", "read_only": True, "comment": ""},
{"name": "other", "path": "/media/wrolpi", "read_only": True, "comment": ""},
])
with mock.patch("controller.lib.samba.save_config"):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba._reload_samba"):
result = remove_share("test")
assert result["success"] is True
from controller.lib.config import get_config_value
assert len(get_config_value("samba.shares", [])) == 1
def test_stops_samba_when_last_share_removed(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "last", "path": "/media/wrolpi", "read_only": True, "comment": ""},
])
with mock.patch("controller.lib.samba.save_config"):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba._stop_samba") as mock_stop:
result = remove_share("last")
assert result["success"] is True
mock_stop.assert_called_once()
def test_returns_error_for_missing_share(self, reset_runtime_config):
result = remove_share("nonexistent")
assert result["success"] is False
assert "not found" in result["error"]
class TestApplySambaFromConfig:
"""Tests for apply_samba_from_config function."""
def test_starts_samba_when_shares_exist(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "test", "path": "/media/wrolpi", "read_only": True, "comment": ""},
])
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.stopped):
with mock.patch("controller.lib.samba._start_samba") as mock_start:
apply_samba_from_config()
mock_start.assert_called_once()
def test_stops_samba_when_no_shares(self, reset_runtime_config):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.running):
with mock.patch("controller.lib.samba._stop_samba") as mock_stop:
apply_samba_from_config()
mock_stop.assert_called_once()
def test_does_not_start_if_already_running(self, reset_runtime_config):
from controller.lib.config import update_config
update_config("samba.shares", [
{"name": "test", "path": "/media/wrolpi", "read_only": True, "comment": ""},
])
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.running):
with mock.patch("controller.lib.samba._start_samba") as mock_start:
apply_samba_from_config()
mock_start.assert_not_called()
def test_does_not_stop_if_already_stopped(self, reset_runtime_config):
with mock.patch("controller.lib.samba._write_smb_conf", return_value={"success": True}):
with mock.patch("controller.lib.samba.get_samba_status", return_value=SambaStatus.stopped):
with mock.patch("controller.lib.samba._stop_samba") as mock_stop:
apply_samba_from_config()
mock_stop.assert_not_called()