Files
sabnzbd/tests/test_config.py
2025-11-21 10:00:09 +01:00

298 lines
13 KiB
Python

#!/usr/bin/python3 -OO
# Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
tests.test_config - Tests of config methods
"""
from sabnzbd.filesystem import long_path
from tests.testhelper import *
import shutil
import zipfile
import os
import sabnzbd.cfg
from sabnzbd.constants import (
DEF_INI_FILE,
DEF_HTTPS_CERT_FILE,
DEF_HTTPS_KEY_FILE,
CONFIG_BACKUP_HTTPS,
CONFIG_BACKUP_FILES,
)
from sabnzbd import config
from sabnzbd import filesystem
DEF_CHAIN_FILE = "server.chain"
class TestOptions:
test_section = "test_section"
test_keyword = "test_keyword"
def test_base_option(self):
test_option = config.Option(self.test_section, self.test_keyword)
assert test_option.section == self.test_section
assert test_option.keyword == self.test_keyword
assert test_option.section in config.CFG_DATABASE
assert test_option.keyword in config.CFG_DATABASE[test_option.section]
assert config.CFG_DATABASE[test_option.section][test_option.keyword] == test_option
# Reset database
config.CFG_DATABASE = {}
@pytest.mark.xfail(reason="These tests should be added")
def test_all(self):
# Need to add tests for all the relevant options
raise NotImplemented
def test_non_public(self):
test_option = config.Option(self.test_section, self.test_keyword, public=True)
assert test_option.get_dict() == {self.test_keyword: None}
assert test_option.get_dict(for_public_api=False) == {self.test_keyword: None}
test_option = config.Option(self.test_section, self.test_keyword, public=False)
assert test_option.get_dict() == {self.test_keyword: None}
assert test_option.get_dict(for_public_api=True) == {}
# Password is special when using for_public_api
test_option = config.OptionPassword(self.test_section, self.test_keyword, default_val="test_password")
assert test_option.get_dict() == {self.test_keyword: "test_password"}
assert test_option.get_dict(for_public_api=True) == {self.test_keyword: "**********"}
# Reset database
config.CFG_DATABASE = {}
@pytest.mark.usefixtures("clean_cache_dir")
class TestConfig:
@staticmethod
def create_dummy_zip(filename: str) -> bytes:
with io.BytesIO() as zip_buffer:
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_ref:
zip_ref.writestr(filename, "foobar")
return zip_buffer.getvalue()
@staticmethod
def create_and_verify_backup(admin_dir: str, must_haves: list[str]):
# Create the backup
config_backup_path = config.create_config_backup()
assert os.path.exists(config_backup_path)
assert sabnzbd.__version__ in config_backup_path
assert time.strftime("%Y.%m.%d_%H") in config_backup_path
# Verify the zipfile has the expected content
with open(config_backup_path, "rb") as fp:
# Do basic backup validation
assert config.validate_config_backup(fp.read())
# Reset the file pointer
fp.seek(0)
with zipfile.ZipFile(fp, "r") as zip:
for basename in must_haves:
assert zip.getinfo(basename)
# Make sure there's nothing else in the zip
assert (zip_len := len(zip.filelist)) == len(must_haves)
# Move the current admin dir out of the way
stowed_admin = os.path.join(SAB_CACHE_DIR, "stowed_admin")
if os.path.isdir(stowed_admin):
filesystem.remove_all(stowed_admin)
assert not os.path.exists(stowed_admin)
os.rename(admin_dir, stowed_admin)
assert os.path.exists(stowed_admin)
assert filesystem.globber(stowed_admin) != []
assert not os.path.exists(admin_dir)
filesystem.create_all_dirs(admin_dir)
assert os.path.exists(admin_dir)
assert filesystem.globber(admin_dir) == []
# Store current test settings, as these may change when restoring a backup
restore_me = {setting: getattr(sabnzbd.cfg, setting)() for setting in CONFIG_BACKUP_HTTPS.values()}
# Restore the backup
with open(config_backup_path, "rb") as config_backup_fp:
config.restore_config_backup(config_backup_fp.read())
# Check settings results
restore_changed_settings = False
for filename, setting in CONFIG_BACKUP_HTTPS.items():
if filename in must_haves:
restore_changed_settings = True
value = getattr(sabnzbd.cfg, setting)()
if setting != "https_chain":
# All https settings should point to the default basenames of the restored files...
assert value == getattr(sabnzbd.cfg, setting).default
else:
# ...except the one that doesn't have a default and uses a hardcoded filename instead
assert value == DEF_CHAIN_FILE
# Check filename results
for basename in must_haves:
# Verify all files in the backup were restored into the admin dir...
assert os.path.exists(os.path.join(admin_dir, basename))
# ...and nothing else
if not restore_changed_settings:
assert zip_len == len(filesystem.globber(admin_dir))
else:
# Account for sabnzbd.ini.bak in case settings were changed as part of the restore
assert zip_len + 1 == len(filesystem.globber(admin_dir))
# Restore the test settings
for setting, value in restore_me.items():
getattr(sabnzbd.cfg, setting).set(value)
sabnzbd.config.save_config(True)
# Purge the backup file to prevent collisions
os.unlink(config_backup_path)
assert not os.path.exists(config_backup_path)
# Call the original admin dir back into active duty
filesystem.remove_all(admin_dir)
assert not os.path.exists(admin_dir)
os.rename(stowed_admin, admin_dir)
assert os.path.exists(admin_dir)
assert filesystem.globber(admin_dir) != []
assert not os.path.exists(stowed_admin)
def test_validate_config_backup(self):
"""Validate basic dummy data"""
assert not config.validate_config_backup(b"invalid")
assert not config.validate_config_backup(self.create_dummy_zip("dummyfile"))
assert config.validate_config_backup(self.create_dummy_zip(DEF_INI_FILE))
@set_config(
{
"admin_dir": os.path.join(SAB_CACHE_DIR, "test_config_backup"),
"complete_dir": os.path.join(SAB_COMPLETE_DIR, "test_config_backup"),
}
)
def test_config_backup(self):
"""Combined tests for the config.{create,validate,restore}_config_backup functions"""
# Prepare the basics
admin_dir = sabnzbd.cfg.admin_dir.get_path()
sabnzbd.cfg.set_root_folders2()
ini_path = os.path.join(admin_dir, DEF_INI_FILE)
shutil.copyfile(os.path.join(SAB_DATA_DIR, "sabnzbd.basic.ini"), ini_path)
assert os.path.exists(ini_path)
config.read_config(ini_path)
filesystem.create_all_dirs(sabnzbd.cfg.complete_dir())
assert os.path.exists(sabnzbd.cfg.complete_dir())
# Create a backup and verify it has the expected files (ini only, as there are no admin and https config files)
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE])
# Add other admin files that qualify for inclusion in backups
for basename in CONFIG_BACKUP_FILES:
with open(admin_file := os.path.join(admin_dir, basename), "wb") as fp:
fp.write(os.urandom(128))
assert os.path.exists(admin_file)
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE] + CONFIG_BACKUP_FILES)
# Add some useless files in the admin_dir
for basename in ["totals3.sab", "Best.Movie.Ever.1951.240p.avi", "Rating.sab"]:
with open(useless_file := os.path.join(admin_dir, basename), "wb") as fp:
fp.write(os.urandom(256))
assert os.path.exists(useless_file)
# None of these should appear in the backup
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE] + CONFIG_BACKUP_FILES)
# Remove the extra admin files, but keep the useless ones around
for basename in CONFIG_BACKUP_FILES:
os.unlink(admin_file := os.path.join(admin_dir, basename))
assert not os.path.exists(admin_file)
# Generate fake HTTPS certificate and key files
cert_file = os.path.join(admin_dir, DEF_HTTPS_CERT_FILE)
key_file = os.path.join(admin_dir, DEF_HTTPS_KEY_FILE)
for filepath in (cert_file, key_file):
with open(filepath, "wb") as fp:
fp.write(os.urandom(512))
assert os.path.exists(cert_file)
assert os.path.exists(key_file)
# Copy cert and key to create a second set of https config files outside the admin dir
other_cert_file = long_path(os.path.join(SAB_CACHE_DIR, "foobar.mycert"))
other_key_file = long_path(os.path.join(SAB_CACHE_DIR, "foobar.mykey"))
shutil.copyfile(cert_file, other_key_file)
shutil.copyfile(key_file, other_cert_file)
assert os.path.exists(other_cert_file)
assert os.path.exists(other_key_file)
# Imitate a mainstream https setup (cert and key present, but no chain file)
sabnzbd.cfg.enable_https.set(True)
sabnzbd.cfg.https_cert.set(DEF_HTTPS_CERT_FILE)
sabnzbd.cfg.https_key.set(DEF_HTTPS_KEY_FILE)
sabnzbd.config.save_config(True)
assert not sabnzbd.cfg.https_chain()
assert sabnzbd.CONFIG_BACKUP_HTTPS_OK == []
# Results should remain the same, as we didn't fake the results of a startup with https enabled yet
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE])
# Results should still remain the same, the startup data lists only bogus files
sabnzbd.CONFIG_BACKUP_HTTPS_OK = ["/tmp/no.cert", "/lib/fuldstændig_falsk.nøgle", "/etc/存在しないファイル"]
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE])
# Now pretend the program started with this config (note: full paths must be used for _OK)
sabnzbd.CONFIG_BACKUP_HTTPS_OK = [cert_file, key_file]
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE, DEF_HTTPS_CERT_FILE, DEF_HTTPS_KEY_FILE])
# Pretend some other files were loaded on startup instead
sabnzbd.CONFIG_BACKUP_HTTPS_OK = [other_cert_file, other_key_file]
# Files in the settings no longer match those in _OK; no https config should be in the backup
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE])
# Set the full path to a key and cert file outside the admin dir
sabnzbd.cfg.https_cert.set(other_cert_file)
sabnzbd.cfg.https_key.set(other_key_file)
sabnzbd.config.save_config(True)
# Now the files should be included, albeit under the default names
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE, DEF_HTTPS_CERT_FILE, DEF_HTTPS_KEY_FILE])
# Repeat with the "others" removed, so there's nothing (but the ini) left to include in the first place
for f in (other_cert_file, other_key_file):
os.unlink(f)
assert not os.path.exists(other_cert_file)
assert not os.path.exists(other_key_file)
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE])
# Make up a chain file
chain_file = os.path.join(admin_dir, "ssl-chain.txt")
shutil.copyfile(cert_file, chain_file)
assert os.path.exists(chain_file)
# Update the config and the startup record (mostly)
sabnzbd.cfg.https_cert.set(cert_file)
sabnzbd.cfg.https_key.set(key_file)
sabnzbd.cfg.https_chain.set(chain_file)
sabnzbd.config.save_config(True)
sabnzbd.CONFIG_BACKUP_HTTPS_OK = [cert_file, key_file]
# There may be a chain file now, but as long as it's not listed in _OK it should be excluded from the backup
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE, DEF_HTTPS_CERT_FILE, DEF_HTTPS_KEY_FILE])
# Now it should be included
sabnzbd.CONFIG_BACKUP_HTTPS_OK.append(chain_file)
self.create_and_verify_backup(
admin_dir, [DEF_INI_FILE, DEF_HTTPS_CERT_FILE, DEF_HTTPS_KEY_FILE, DEF_CHAIN_FILE]
)
# Same same but more lonely
sabnzbd.CONFIG_BACKUP_HTTPS_OK = [chain_file, "/tmp/foobar.exe"]
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE, DEF_CHAIN_FILE])
# Disabling https shouldn't make any difference as long as the evidence shows it was active on startup
sabnzbd.cfg.enable_https.set(False)
sabnzbd.config.save_config(True)
self.create_and_verify_backup(admin_dir, [DEF_INI_FILE, DEF_CHAIN_FILE])