diff --git a/pyproject.toml b/pyproject.toml index b09f128..b99d9cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,3 +46,8 @@ packages = ["simple_backup"] [tool.setuptools.dynamic] version = { attr = "simple_backup.__version__" } + +[tool.pytest.ini_options] +testpaths = ["tests"] +# Import the repo copy, not an installed one +pythonpath = ["."] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..64d3123 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +"""Shared fixtures for the simple_backup tests""" + +import pytest + +from simple_backup import simple_backup as sb + + +@pytest.fixture(autouse=True) +def no_side_effects(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep tests out of the systemd journal and the desktop notifications""" + + if hasattr(sb, 'j_handler'): + sb.logger.removeHandler(sb.j_handler) + + monkeypatch.setattr(sb, '_notify', lambda text: None) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..f48d6c7 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,229 @@ +"""Tests for reading the configuration file and expanding paths""" + +import configparser +import os +import pwd +from pathlib import Path + +import pytest + +from simple_backup import simple_backup as sb + +USER = pwd.getpwuid(os.getuid()).pw_name +HOME = pwd.getpwuid(os.getuid()).pw_dir + + +def write_config(path: Path, text: str) -> str: + config_file = path / 'simple_backup.conf' + config_file.write_text(text) + + return str(config_file) + + +def test_repo_copy_imported() -> None: + repo = Path(__file__).resolve().parents[1] + + assert Path(sb.__file__).resolve().is_relative_to(repo) + + +@pytest.mark.parametrize('path, expected', [ + ('~', HOME), + ('~/docs', f'{HOME}/docs'), + ('/abs/~/path', '/abs/~/path'), + ('relative', 'relative'), +]) +def test_expand_home(path: str, expected: str) -> None: + assert sb._expand_home(path, USER) == expected + + +def test_expand_inputs_glob(tmp_path: Path) -> None: + for name in ['a.txt', 'b.txt', 'c.log']: + (tmp_path / name).touch() + + result = sb._expand_inputs([f'{tmp_path}/*.txt', ''], USER) + + assert sorted(result) == [f'{tmp_path}/a.txt', f'{tmp_path}/b.txt'] + + +def test_expand_inputs_no_match(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + assert sb._expand_inputs([f'{tmp_path}/missing'], USER) == [] + assert 'No file or directory matching input' in caplog.text + + +def test_expand_inputs_tilde_without_user(caplog: pytest.LogCaptureFixture) -> None: + sb._expand_inputs(['~/nothing-here'], None) + + assert 'Cannot expand' in caplog.text + + +def test_missing_config(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + config_args = sb._read_config(str(tmp_path / 'missing.conf'), USER) + + assert config_args['inputs'] is None + assert config_args['output'] is None + assert config_args['keep'] == -1 + assert 'does not exist' in caplog.text + + +def test_missing_config_without_user(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + sb._read_config(str(tmp_path / 'missing.conf'), None) + + assert 'User not specified' in caplog.text + + +def test_read_config(tmp_path: Path) -> None: + (tmp_path / 'one').mkdir() + (tmp_path / 'two').mkdir() + + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path}/one, {tmp_path}/two, {tmp_path}/one +backup_dir=~/backups +exclude=*.bak, , *.tmp +keep=3 +""") + + config_args = sb._read_config(config_file, USER) + + assert sorted(config_args['inputs']) == [f'{tmp_path}/one', f'{tmp_path}/two'] + # Expanded later, as it may be a path on the server + assert config_args['output'] == '~/backups' + assert config_args['exclude'] == ['*.bak', '*.tmp'] + assert config_args['keep'] == 3 + assert config_args['ssh_host'] is None + assert config_args['ssh_user'] is None + assert config_args['ssh_keyfile'] is None + assert config_args['remote_sudo'] is False + assert config_args['numeric_ids'] is False + + +def test_optional_keys_missing(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=/backups +""") + + config_args = sb._read_config(config_file, USER) + + assert config_args['exclude'] == [] + assert config_args['keep'] == -1 + + +def test_old_default_section(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[default] +inputs={tmp_path} +backup_dir=/backups +""") + + config_args = sb._read_config(config_file, USER) + + assert config_args['inputs'] == [str(tmp_path)] + assert config_args['output'] == '/backups' + + +def test_percent_in_values(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=/backups/100% +exclude=%temp% +""") + + config_args = sb._read_config(config_file, USER) + + assert config_args['output'] == '/backups/100%' + assert config_args['exclude'] == ['%temp%'] + + +def test_server_section(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=backups + +[server] +ssh_host=server +ssh_user=user +ssh_keyfile=/keys/id_ed25519 +remote_sudo=yes +numeric_ids=true +""") + + config_args = sb._read_config(config_file, USER) + + assert config_args['ssh_host'] == 'server' + assert config_args['ssh_user'] == 'user' + assert config_args['ssh_keyfile'] == '/keys/id_ed25519' + assert config_args['remote_sudo'] is True + assert config_args['numeric_ids'] is True + + +def test_empty_server_values(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=/backups + +[server] +ssh_host= +ssh_user= +ssh_keyfile= +remote_sudo= +numeric_ids= +""") + + config_args = sb._read_config(config_file, USER) + + for key in ['ssh_host', 'ssh_user', 'ssh_keyfile']: + assert config_args[key] is None + + assert config_args['remote_sudo'] is False + assert config_args['numeric_ids'] is False + + +def test_only_ssh_host(tmp_path: Path) -> None: + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=/backups + +[server] +ssh_host=server +""") + + config_args = sb._read_config(config_file, USER) + + # Kept, so that the missing user is reported instead of silently backing up locally + assert config_args['ssh_host'] == 'server' + assert config_args['ssh_user'] is None + + +@pytest.mark.parametrize('line', ['keep=abc', 'remote_sudo=maybe']) +def test_bad_value(tmp_path: Path, line: str) -> None: + section = '[server]' if line.startswith('remote_sudo') else '' + + config_file = write_config(tmp_path, f""" +[backup] +inputs={tmp_path} +backup_dir=/backups +{section} +{line} +""") + + with pytest.raises(ValueError): + sb._read_config(config_file, USER) + + +@pytest.mark.parametrize('text', [ + '[backup]\nbackup_dir=/backups\n', + '[backup]\ninputs=/tmp\n', + '[other]\ninputs=/tmp\n', + 'inputs=/tmp\n', +]) +def test_bad_config(tmp_path: Path, text: str) -> None: + config_file = write_config(tmp_path, text) + + with pytest.raises(configparser.Error): + sb._read_config(config_file, USER)