ytptube/app/tests/test_presets.py
arabcoders 669f70d9b8 fixes
2025-09-12 19:34:12 +03:00

599 lines
26 KiB
Python

import json
import tempfile
import uuid
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from app.library.Presets import DEFAULT_PRESETS, Preset, Presets
class TestPreset:
"""Test the Preset dataclass."""
def test_preset_creation_with_defaults(self):
"""Test creating a preset with default values."""
preset = Preset(name="test_preset")
assert preset.name == "test_preset"
assert preset.description == ""
assert preset.folder == ""
assert preset.template == ""
assert preset.cookies == ""
assert preset.cli == ""
assert preset.default is False
# ID should be auto-generated UUID
assert len(preset.id) == 36 # UUID4 string length
assert "-" in preset.id
def test_preset_creation_with_all_fields(self):
"""Test creating a preset with all fields specified."""
preset_id = str(uuid.uuid4())
preset = Preset(
id=preset_id,
name="test_preset",
description="Test description",
folder="test_folder",
template="test_template",
cookies="test_cookies",
cli="--test-option",
default=True,
)
assert preset.id == preset_id
assert preset.name == "test_preset"
assert preset.description == "Test description"
assert preset.folder == "test_folder"
assert preset.template == "test_template"
assert preset.cookies == "test_cookies"
assert preset.cli == "--test-option"
assert preset.default is True
def test_preset_serialize(self):
"""Test preset serialization to dictionary."""
preset = Preset(name="test_preset", description="Test description", cli="--test-option")
serialized = preset.serialize()
assert isinstance(serialized, dict)
assert serialized["name"] == "test_preset"
assert serialized["description"] == "Test description"
assert serialized["cli"] == "--test-option"
assert "id" in serialized
def test_preset_json(self):
"""Test preset JSON serialization."""
preset = Preset(name="test_preset", cli="--test-option")
json_str = preset.json()
assert isinstance(json_str, str)
# Should be valid JSON
parsed = json.loads(json_str)
assert parsed["name"] == "test_preset"
assert parsed["cli"] == "--test-option"
def test_preset_get_method(self):
"""Test preset get method for accessing fields."""
preset = Preset(name="test_preset", description="Test description")
assert preset.get("name") == "test_preset"
assert preset.get("description") == "Test description"
assert preset.get("nonexistent") is None
assert preset.get("nonexistent", "default_value") == "default_value"
def test_preset_id_generation(self):
"""Test that each preset gets a unique ID."""
preset1 = Preset(name="preset1")
preset2 = Preset(name="preset2")
assert preset1.id != preset2.id
assert len(preset1.id) == 36
assert len(preset2.id) == 36
class TestPresets:
"""Test the Presets singleton manager."""
def setup_method(self):
"""Set up test environment before each test."""
# Reset singleton completely
Presets._reset_singleton()
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_singleton(self, mock_eventbus, mock_config):
"""Test that Presets follows singleton pattern."""
mock_config.get_instance.return_value = Mock(config_path="/tmp", default_preset="default")
mock_eventbus.get_instance.return_value = Mock()
instance1 = Presets.get_instance()
instance2 = Presets.get_instance()
assert instance1 is instance2
assert isinstance(instance1, Presets)
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_initialization(self, mock_eventbus, mock_config):
"""Test Presets initialization with default presets."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus_instance = Mock()
mock_eventbus.get_instance.return_value = mock_eventbus_instance
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Should have default presets loaded
default_presets = presets._default
assert len(default_presets) > 0
# Check that default presets are Preset instances
for preset in default_presets:
assert isinstance(preset, Preset)
assert preset.default is True
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_load_empty_file(self, mock_eventbus, mock_config):
"""Test loading presets from empty or non-existent file."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
presets.clear() # Ensure we start with empty items
result = presets.load()
assert result is presets
assert len(presets._items) == 0
assert len(presets._default) > 0 # Should still have default presets
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.arg_converter")
def test_presets_load_valid_file(self, mock_arg_converter, mock_eventbus, mock_config):
"""Test loading presets from valid JSON file."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_arg_converter.return_value = [] # Mock successful CLI parsing
test_presets = [
{"id": str(uuid.uuid4()), "name": "test_preset_1", "description": "Test preset 1", "cli": "--format best"},
{"id": str(uuid.uuid4()), "name": "test_preset_2", "description": "Test preset 2", "cli": "--format worst"},
]
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets_file.write_text(json.dumps(test_presets, indent=2))
presets = Presets(file=presets_file, config=mock_config_instance)
presets._items.clear() # Ensure we start with empty items
result = presets.load()
assert result is presets
assert len(presets._items) == 2
assert presets._items[0].name == "test_preset_1"
assert presets._items[1].name == "test_preset_2"
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.Config")
def test_presets_load_invalid_json(self, mock_eventbus, mock_config):
"""Test loading presets from invalid JSON file."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets_file.write_text("invalid json content")
presets = Presets(file=presets_file, config=mock_config_instance)
# Should handle invalid JSON gracefully
result = presets.load()
assert result is presets
assert len(presets._items) == 0
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.arg_converter")
def test_presets_load_with_format_migration(self, mock_arg_converter, mock_eventbus, mock_config):
"""Test loading presets with old format that needs migration."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_arg_converter.return_value = [] # Mock successful CLI parsing
# Old format preset with 'format' field instead of 'cli'
old_preset = {"name": "old_preset", "format": "best[height<=720]", "description": "Old format preset"}
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets_file.write_text(json.dumps([old_preset]))
presets = Presets(file=presets_file, config=mock_config_instance)
result = presets.load()
assert result is presets
assert len(presets._items) == 1
loaded_preset = presets._items[0]
assert loaded_preset.name == "old_preset"
# Should have migrated format to cli
assert "best[height<=720]" in loaded_preset.cli
assert "--format" in loaded_preset.cli
# Should have generated ID
assert loaded_preset.id is not None
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_validate_valid_preset(self, mock_eventbus, mock_config):
"""Test validating a valid preset."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
valid_preset = {"id": str(uuid.uuid4()), "name": "valid_preset", "cli": "--format best"}
result = presets.validate(valid_preset)
assert result is True
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_validate_invalid_preset(self, mock_eventbus, mock_config):
"""Test validating invalid presets."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Test missing ID
with pytest.raises(ValueError, match="No id found"):
presets.validate({"name": "test"})
# Test missing name
with pytest.raises(ValueError, match="No name found"):
presets.validate({"id": str(uuid.uuid4())})
# Test wrong type
with pytest.raises(ValueError, match="Unexpected"):
presets.validate("invalid_type")
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.arg_converter")
def test_presets_save(self, mock_arg_converter, mock_eventbus, mock_config):
"""Test saving presets to file."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_arg_converter.return_value = [] # Mock successful CLI parsing
test_presets = [
Preset(name="test1", cli="--format best"),
Preset(name="test2", cli="--format worst", default=True), # This should be excluded from save
]
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
result = presets.save(test_presets)
assert result is presets
assert presets_file.exists()
# Should only save non-default presets
saved_data = json.loads(presets_file.read_text())
assert len(saved_data) == 1
assert saved_data[0]["name"] == "test1"
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_get_all(self, mock_eventbus, mock_config):
"""Test getting all presets (default + custom)."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Add a custom preset
custom_preset = Preset(name="custom", cli="--custom")
presets._items.append(custom_preset)
all_presets = presets.get_all()
# Should include both default and custom presets
assert len(all_presets) > 1
assert any(p.name == "custom" for p in all_presets)
assert any(p.default is True for p in all_presets)
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_get_by_id(self, mock_eventbus, mock_config):
"""Test getting preset by ID."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
test_id = str(uuid.uuid4())
test_preset = Preset(id=test_id, name="test_preset")
presets._items.append(test_preset)
found_preset = presets.get(test_id)
assert found_preset is not None
assert found_preset.id == test_id
assert found_preset.name == "test_preset"
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_get_by_name(self, mock_eventbus, mock_config):
"""Test getting preset by name."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
test_preset = Preset(name="test_preset", cli="--test")
presets._items.append(test_preset)
found_preset = presets.get("test_preset")
assert found_preset is not None
assert found_preset.name == "test_preset"
assert found_preset.cli == "--test"
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_get_nonexistent(self, mock_eventbus, mock_config):
"""Test getting non-existent preset returns None."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
found_preset = presets.get("nonexistent")
assert found_preset is None
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_get_empty_parameter(self, mock_eventbus, mock_config):
"""Test getting preset with empty string returns None."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
assert presets.get("") is None
assert presets.get(None) is None
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_has_method(self, mock_eventbus, mock_config):
"""Test has method for checking preset existence."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
test_preset = Preset(name="existing_preset")
presets._items.append(test_preset)
assert presets.has("existing_preset") is True
assert presets.has(test_preset.id) is True
assert presets.has("nonexistent") is False
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_clear(self, mock_eventbus, mock_config):
"""Test clearing all custom presets."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Add some custom presets
presets._items.append(Preset(name="custom1"))
presets._items.append(Preset(name="custom2"))
assert len(presets._items) == 2
result = presets.clear()
assert result is presets
assert len(presets._items) == 0
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_file_permissions(self, mock_eventbus, mock_config):
"""Test that presets file gets correct permissions."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
# Create file with different permissions
presets_file.write_text("{}")
presets_file.chmod(0o644)
# Creating Presets instance should attempt to fix permissions
presets = Presets(file=presets_file, config=mock_config_instance)
# Note: The actual chmod might fail in test environment,
# but we're testing that the code attempts it
assert presets is not None
def test_default_presets_validation(self):
"""Test that all default presets are valid."""
# This test verifies that the DEFAULT_PRESETS constant contains valid presets
for i, preset_data in enumerate(DEFAULT_PRESETS):
assert "id" in preset_data, f"Default preset {i} missing id"
assert "name" in preset_data, f"Default preset {i} missing name"
assert "default" in preset_data, f"Default preset {i} missing default flag"
assert preset_data["default"] is True, f"Default preset {i} should have default=True"
# Should be able to create Preset instance
preset = Preset(
id=preset_data["id"],
name=preset_data["name"],
description=preset_data.get("description", ""),
folder=preset_data.get("folder", ""),
template=preset_data.get("template", ""),
cli=preset_data.get("cli", ""),
default=preset_data["default"],
)
assert isinstance(preset, Preset)
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_attach_method(self, mock_eventbus, mock_config):
"""Test the attach method for aiohttp integration."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_app = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Mock the get method to return a preset for default_preset check
with patch.object(presets, "get", return_value=Mock()) as mock_get:
presets.attach(mock_app)
mock_get.assert_called_once_with("default")
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
def test_presets_attach_default_preset_not_found(self, mock_eventbus, mock_config):
"""Test attach method when default preset is not found."""
mock_config_instance = Mock(config_path="/tmp", default_preset="nonexistent")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_app = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
with patch.object(presets, "get", return_value=None):
# The actual config instance stored in presets should be checked
presets.attach(mock_app)
# Should have reset default_preset to "default"
assert presets._config.default_preset == "default"
@pytest.mark.asyncio
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
async def test_presets_on_shutdown(self, mock_eventbus, mock_config):
"""Test the on_shutdown method."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_app = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
# Should not raise an exception
await presets.on_shutdown(mock_app)
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.LOG")
@patch("app.library.Presets.arg_converter")
def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_eventbus, mock_config):
"""Test loading file with some invalid presets."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
mock_arg_converter.return_value = [] # Mock successful CLI parsing
# Mix of valid and invalid presets
test_presets = [
{"id": str(uuid.uuid4()), "name": "valid_preset", "cli": "--format best"},
{
# Missing required fields
"description": "Invalid preset"
},
]
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets_file.write_text(json.dumps(test_presets))
# Clear any existing items first
presets = Presets(file=presets_file, config=mock_config_instance)
presets.load()
# Should only load the valid preset
assert len(presets._items) == 1
assert presets._items[0].name == "valid_preset", f"Expected 'valid_preset', got '{presets._items}'"
# Should have logged an error for the invalid preset
mock_log.error.assert_called()
@patch("app.library.Presets.Config")
@patch("app.library.Presets.EventBus")
@patch("app.library.Presets.LOG")
def test_presets_save_with_invalid_preset(self, mock_log, mock_eventbus, mock_config):
"""Test saving presets with some invalid ones."""
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
mock_config.get_instance.return_value = mock_config_instance
mock_eventbus.get_instance.return_value = Mock()
# Create preset with invalid CLI that will fail validation
valid_preset = Preset(name="valid", cli="--valid-option")
# Create invalid preset data
invalid_preset_data = {"name": "invalid"} # Missing ID
presets_to_save = [valid_preset, invalid_preset_data]
with tempfile.TemporaryDirectory() as temp_dir:
presets_file = Path(temp_dir) / "presets.json"
presets = Presets(file=presets_file, config=mock_config_instance)
presets.save(presets_to_save)
# Should have logged errors for invalid presets
mock_log.error.assert_called()