Files
fenrir/tests/unit/test_settings_validation.py
T

293 lines
12 KiB
Python

"""
Unit tests for settings validation in SettingsManager.
Tests the _validate_setting_value method to ensure proper input validation
for all configurable settings that could cause crashes or accessibility issues.
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock
# Import the settings manager
from fenrirscreenreader.core.settingsData import settings_data
from fenrirscreenreader.core.settingsManager import SettingsManager
from fenrirscreenreader.commands.commands import save_settings
@pytest.mark.unit
@pytest.mark.settings
class TestSpeechSettingsValidation:
"""Test validation of speech-related settings."""
def setup_method(self):
"""Create a SettingsManager instance for each test."""
self.manager = SettingsManager()
def test_speech_rate_valid_range(self):
"""Speech rate should accept values between 0.0 and 3.0."""
# Valid boundary values
self.manager._validate_setting_value("speech", "rate", 0.0)
self.manager._validate_setting_value("speech", "rate", 1.5)
self.manager._validate_setting_value("speech", "rate", 3.0)
def test_speech_rate_rejects_negative(self):
"""Speech rate should reject negative values."""
with pytest.raises(ValueError, match="must be between 0.0 and 3.0"):
self.manager._validate_setting_value("speech", "rate", -0.1)
def test_speech_rate_rejects_too_high(self):
"""Speech rate should reject values above 3.0."""
with pytest.raises(ValueError, match="must be between 0.0 and 3.0"):
self.manager._validate_setting_value("speech", "rate", 10.0)
def test_speech_pitch_valid_range(self):
"""Speech pitch should accept values between 0.0 and 2.0."""
self.manager._validate_setting_value("speech", "pitch", 0.0)
self.manager._validate_setting_value("speech", "pitch", 1.0)
self.manager._validate_setting_value("speech", "pitch", 2.0)
def test_speech_pitch_rejects_invalid(self):
"""Speech pitch should reject out-of-range values."""
with pytest.raises(ValueError, match="must be between 0.0 and 2.0"):
self.manager._validate_setting_value("speech", "pitch", -1.0)
with pytest.raises(ValueError, match="must be between 0.0 and 2.0"):
self.manager._validate_setting_value("speech", "pitch", 5.0)
def test_speech_volume_valid_range(self):
"""Speech volume should accept values between 0.0 and 1.5."""
self.manager._validate_setting_value("speech", "volume", 0.0)
self.manager._validate_setting_value("speech", "volume", 1.0)
self.manager._validate_setting_value("speech", "volume", 1.5)
def test_speech_volume_rejects_negative(self):
"""Speech volume should reject negative values."""
with pytest.raises(ValueError, match="must be between 0.0 and 1.5"):
self.manager._validate_setting_value("speech", "volume", -0.5)
def test_speech_driver_whitelisted(self):
"""Speech driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("speech", "driver", "speechdDriver")
self.manager._validate_setting_value("speech", "driver", "genericDriver")
self.manager._validate_setting_value("speech", "driver", "dummyDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid speech driver"):
self.manager._validate_setting_value(
"speech", "driver", "nonexistentDriver"
)
@pytest.mark.unit
@pytest.mark.settings
class TestSoundSettingsValidation:
"""Test validation of sound-related settings."""
def setup_method(self):
"""Create a SettingsManager instance for each test."""
self.manager = SettingsManager()
def test_sound_volume_valid_range(self):
"""Sound volume should accept values between 0.0 and 1.5."""
self.manager._validate_setting_value("sound", "volume", 0.0)
self.manager._validate_setting_value("sound", "volume", 0.7)
self.manager._validate_setting_value("sound", "volume", 1.5)
def test_sound_volume_rejects_invalid(self):
"""Sound volume should reject out-of-range values."""
with pytest.raises(ValueError, match="must be between 0.0 and 1.5"):
self.manager._validate_setting_value("sound", "volume", -0.1)
with pytest.raises(ValueError, match="must be between 0.0 and 1.5"):
self.manager._validate_setting_value("sound", "volume", 2.0)
def test_sound_driver_whitelisted(self):
"""Sound driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("sound", "driver", "genericDriver")
self.manager._validate_setting_value("sound", "driver", "gstreamerDriver")
self.manager._validate_setting_value("sound", "driver", "dummyDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid sound driver"):
self.manager._validate_setting_value("sound", "driver", "invalidDriver")
@pytest.mark.unit
@pytest.mark.settings
class TestDriverValidation:
"""Test validation of driver selection settings."""
def setup_method(self):
"""Create a SettingsManager instance for each test."""
self.manager = SettingsManager()
def test_screen_driver_whitelisted(self):
"""Screen driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("screen", "driver", "vcsaDriver")
self.manager._validate_setting_value("screen", "driver", "ptyDriver")
self.manager._validate_setting_value("screen", "driver", "dummyDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid screen driver"):
self.manager._validate_setting_value("screen", "driver", "unknownDriver")
def test_keyboard_driver_whitelisted(self):
"""Keyboard driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("keyboard", "driver", "evdevDriver")
self.manager._validate_setting_value("keyboard", "driver", "x11Driver")
self.manager._validate_setting_value("keyboard", "driver", "dummyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "ptyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "atspiDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "badDriver")
@pytest.mark.unit
@pytest.mark.settings
class TestGeneralSettingsValidation:
"""Test validation of general settings."""
def setup_method(self):
"""Create a SettingsManager instance for each test."""
self.manager = SettingsManager()
def test_debug_level_valid_range(self):
"""Debug level should accept values 0-3."""
self.manager._validate_setting_value("general", "debug_level", 0)
self.manager._validate_setting_value("general", "debug_level", 1)
self.manager._validate_setting_value("general", "debug_level", 2)
self.manager._validate_setting_value("general", "debug_level", 3)
def test_debug_level_rejects_invalid(self):
"""Debug level should reject values outside 0-3."""
with pytest.raises(ValueError, match="must be between 0 and 3"):
self.manager._validate_setting_value("general", "debug_level", -1)
with pytest.raises(ValueError, match="must be between 0 and 3"):
self.manager._validate_setting_value("general", "debug_level", 10)
@pytest.mark.unit
@pytest.mark.settings
class TestValidationSkipsUnknownSettings:
"""Test that validation doesn't error on unknown settings."""
def setup_method(self):
"""Create a SettingsManager instance for each test."""
self.manager = SettingsManager()
def test_unknown_section_no_error(self):
"""Unknown sections should not raise errors during validation."""
# Should not raise - validation only applies to known critical settings
self.manager._validate_setting_value("unknown_section", "setting", "value")
def test_unknown_setting_no_error(self):
"""Unknown settings in known sections should not raise errors."""
# Should not raise - only specific critical settings are validated
self.manager._validate_setting_value("speech", "unknown_setting", "value")
@pytest.mark.unit
@pytest.mark.settings
def test_focus_settings_define_tui_toggle():
"""Focus settings should include the TUI toggle used by on-screen handlers."""
assert settings_data["focus"]["tui"] is False
@pytest.mark.unit
@pytest.mark.settings
class TestSettingsPathSelection:
"""Test root/user settings load and save path selection."""
def setup_method(self):
self.manager = SettingsManager()
def configure_paths(self, tmp_path):
system_root = tmp_path / "etc" / "fenrirscreenreader"
system_file = system_root / "settings" / "settings.conf"
user_file = (
tmp_path
/ "home"
/ "Username"
/ ".local"
/ "share"
/ "stormux"
/ "fenrirscreenreader"
/ "settings"
/ "settings.conf"
)
self.manager.system_settings_root = str(system_root) + "/"
self.manager.system_settings_file = str(system_file)
self.manager.user_settings_file = str(user_file)
return system_file, user_file
def test_non_root_loads_user_settings_when_present(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
user_file.parent.mkdir(parents=True)
user_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.resolve_settings_file() == str(user_file)
def test_non_root_falls_back_to_system_settings(
self, tmp_path, monkeypatch
):
system_file, _user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.resolve_settings_file() == str(system_file)
def test_root_uses_system_settings_even_when_user_settings_exists(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
user_file.parent.mkdir(parents=True)
user_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 0)
assert self.manager.resolve_settings_file() == str(system_file)
def test_save_default_path_follows_effective_user(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.get_default_save_settings_file() == str(user_file)
monkeypatch.setattr("os.geteuid", lambda: 0)
assert self.manager.get_default_save_settings_file() == str(system_file)
def test_save_settings_command_uses_default_save_path(self):
manager = Mock()
output_manager = Mock()
command = save_settings.command()
command.initialize(
{
"runtime": {
"SettingsManager": manager,
"OutputManager": output_manager,
}
}
)
command.run()
manager.save_settings.assert_called_once_with()