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

746 lines
27 KiB
Python

"""
Tests for conditions.py - Download conditions management.
This test suite provides comprehensive coverage for the conditions module:
- Tests Condition dataclass functionality
- Tests Conditions singleton class behavior
- Tests condition loading, saving, and validation
- Tests condition matching against info dicts
- Tests error handling and edge cases
Total test functions: 15+
All condition management functionality and edge cases are covered.
"""
import json
import tempfile
import uuid
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from app.library.conditions import Condition, Conditions
class TestCondition:
"""Test the Condition dataclass."""
def test_condition_creation_with_defaults(self):
"""Test creating a condition with default values."""
condition = Condition(name="test", filter="duration > 60")
# Check that ID is generated
assert condition.id
assert isinstance(condition.id, str)
# Check required fields
assert condition.name == "test"
assert condition.filter == "duration > 60"
# Check defaults
assert condition.cli == ""
assert condition.extras == {}
def test_condition_creation_with_all_fields(self):
"""Test creating a condition with all fields specified."""
test_id = str(uuid.uuid4())
extras = {"key": "value", "number": 42}
condition = Condition(
id=test_id,
name="full_test",
filter="uploader = 'test'",
cli="--format best",
extras=extras
)
assert condition.id == test_id
assert condition.name == "full_test"
assert condition.filter == "uploader = 'test'"
assert condition.cli == "--format best"
assert condition.extras == extras
def test_condition_serialize(self):
"""Test condition serialization to dict."""
condition = Condition(
name="serialize_test",
filter="title ~= 'test'",
cli="--audio-quality 0",
extras={"tag": "music"}
)
serialized = condition.serialize()
assert isinstance(serialized, dict)
assert serialized["name"] == "serialize_test"
assert serialized["filter"] == "title ~= 'test'"
assert serialized["cli"] == "--audio-quality 0"
assert serialized["extras"] == {"tag": "music"}
assert "id" in serialized
def test_condition_json(self):
"""Test condition JSON serialization."""
condition = Condition(name="json_test", filter="duration < 300")
json_str = condition.json()
assert isinstance(json_str, str)
# Should be valid JSON
data = json.loads(json_str)
assert data["name"] == "json_test"
assert data["filter"] == "duration < 300"
def test_condition_get_method(self):
"""Test condition get method for accessing fields."""
condition = Condition(
name="get_test",
filter="view_count > 1000",
extras={"category": "popular"}
)
assert condition.get("name") == "get_test"
assert condition.get("filter") == "view_count > 1000"
assert condition.get("extras") == {"category": "popular"}
assert condition.get("nonexistent") is None
assert condition.get("nonexistent", "default") == "default"
class TestConditions:
"""Test the Conditions singleton class."""
def setup_method(self):
"""Set up test fixtures by clearing singleton instances."""
Conditions._reset_singleton()
def test_conditions_singleton(self):
"""Test that Conditions follows singleton pattern."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "test_conditions.json"
instance1 = Conditions(file=file_path)
instance2 = Conditions.get_instance()
assert instance1 is instance2
def test_conditions_initialization(self):
"""Test Conditions initialization with file path."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "test_conditions.json"
conditions = Conditions(file=file_path)
assert conditions._file == file_path
assert isinstance(conditions._items, list)
@patch("app.library.conditions.Config.get_instance")
def test_conditions_default_file_path(self, mock_config):
"""Test Conditions uses default config path when no file specified."""
mock_config_instance = MagicMock()
mock_config_instance.config_path = "/test/config"
mock_config.return_value = mock_config_instance
conditions = Conditions()
expected_path = Path("/test/config") / "conditions.json"
assert conditions._file == expected_path
def test_get_all_empty(self):
"""Test get_all returns empty list when no conditions loaded."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "empty_conditions.json"
conditions = Conditions(file=file_path)
result = conditions.get_all()
assert isinstance(result, list)
assert len(result) == 0
def test_clear(self):
"""Test clearing all conditions."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "clear_test.json"
conditions = Conditions(file=file_path)
# Add some test conditions
conditions._items = [
Condition(name="test1", filter="duration > 60"),
Condition(name="test2", filter="uploader = 'test'")
]
result = conditions.clear()
assert result is conditions # Should return self
assert len(conditions._items) == 0
def test_clear_when_already_empty(self):
"""Test clearing when conditions list is already empty."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "already_empty.json"
conditions = Conditions(file=file_path)
result = conditions.clear()
assert result is conditions
assert len(conditions._items) == 0
def test_load_nonexistent_file(self):
"""Test loading from non-existent file."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "nonexistent.json"
conditions = Conditions(file=file_path)
result = conditions.load()
assert result is conditions
assert len(conditions._items) == 0
def test_load_empty_file(self):
"""Test loading from empty file."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "empty.json"
file_path.touch() # Create empty file
conditions = Conditions(file=file_path)
result = conditions.load()
assert result is conditions
assert len(conditions._items) == 0
def test_load_valid_conditions(self):
"""Test loading valid conditions from file."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "valid_conditions.json"
# Create test data
test_data = [
{
"id": str(uuid.uuid4()),
"name": "short_videos",
"filter": "duration < 300",
"cli": "--format worst",
"extras": {"category": "short"}
},
{
"id": str(uuid.uuid4()),
"name": "music_videos",
"filter": "title ~= 'music'",
"cli": "--audio-quality 0",
"extras": {"type": "audio"}
}
]
file_path.write_text(json.dumps(test_data, indent=4))
conditions = Conditions(file=file_path)
result = conditions.load()
assert result is conditions
assert len(conditions._items) == 2
# Check first condition
assert conditions._items[0].name == "short_videos"
assert conditions._items[0].filter == "duration < 300"
assert conditions._items[0].cli == "--format worst"
assert conditions._items[0].extras == {"category": "short"}
# Check second condition
assert conditions._items[1].name == "music_videos"
assert conditions._items[1].filter == "title ~= 'music'"
def test_load_conditions_without_id(self):
"""Test loading conditions that don't have ID (should generate ID)."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_id_conditions.json"
# Create test data without ID
test_data = [
{
"name": "no_id_test",
"filter": "duration > 120"
}
]
file_path.write_text(json.dumps(test_data))
with patch.object(Conditions, "save") as mock_save:
conditions = Conditions(file=file_path)
conditions.load()
# Should have generated ID
assert len(conditions._items) == 1
assert conditions._items[0].id
assert conditions._items[0].name == "no_id_test"
# Should call save due to changes
mock_save.assert_called_once()
def test_load_conditions_without_extras(self):
"""Test loading conditions that don't have extras field."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_extras_conditions.json"
test_data = [
{
"id": str(uuid.uuid4()),
"name": "no_extras_test",
"filter": "uploader = 'test'"
}
]
file_path.write_text(json.dumps(test_data))
with patch.object(Conditions, "save") as mock_save:
conditions = Conditions(file=file_path)
conditions.load()
# Should have generated empty extras
assert len(conditions._items) == 1
assert conditions._items[0].extras == {}
# Should call save due to changes
mock_save.assert_called_once()
def test_load_invalid_json(self):
"""Test loading file with invalid JSON."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid.json"
file_path.write_text("invalid json content")
conditions = Conditions(file=file_path)
result = conditions.load()
assert result is conditions
assert len(conditions._items) == 0
def test_load_invalid_condition_data(self):
"""Test loading file with invalid condition data."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_data.json"
# Missing required fields
test_data = [
{"id": "valid", "name": "valid", "filter": "duration > 60"},
{"invalid": "data"} # Missing required fields
]
file_path.write_text(json.dumps(test_data))
conditions = Conditions(file=file_path)
result = conditions.load()
# Should load only valid conditions
assert result is conditions
assert len(conditions._items) == 1
assert conditions._items[0].name == "valid"
def test_validate_valid_condition_dict(self):
"""Test validating a valid condition dictionary."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "validate_test.json"
conditions = Conditions(file=file_path)
valid_condition = {
"id": str(uuid.uuid4()),
"name": "valid_test",
"filter": "duration > 60",
"cli": "--format best",
"extras": {"key": "value"}
}
result = conditions.validate(valid_condition)
assert result is True
def test_validate_valid_condition_object(self):
"""Test validating a valid Condition object."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "validate_obj_test.json"
conditions = Conditions(file=file_path)
valid_condition = Condition(
name="valid_obj_test",
filter="uploader = 'test'"
)
result = conditions.validate(valid_condition)
assert result is True
def test_validate_missing_id(self):
"""Test validating condition without ID."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_id_validate.json"
conditions = Conditions(file=file_path)
invalid_condition = {
"name": "no_id_test",
"filter": "duration > 60"
}
with pytest.raises(ValueError, match="No id found"):
conditions.validate(invalid_condition)
def test_validate_missing_name(self):
"""Test validating condition without name."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_name_validate.json"
conditions = Conditions(file=file_path)
invalid_condition = {
"id": str(uuid.uuid4()),
"filter": "duration > 60"
}
with pytest.raises(ValueError, match="No name found"):
conditions.validate(invalid_condition)
def test_validate_missing_filter(self):
"""Test validating condition without filter."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_filter_validate.json"
conditions = Conditions(file=file_path)
invalid_condition = {
"id": str(uuid.uuid4()),
"name": "no_filter_test"
}
with pytest.raises(ValueError, match="No filter found"):
conditions.validate(invalid_condition)
def test_validate_invalid_filter(self):
"""Test validating condition with invalid filter syntax."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_filter.json"
conditions = Conditions(file=file_path)
# Use a filter that will cause a syntax error in the parser
invalid_condition = {
"id": str(uuid.uuid4()),
"name": "invalid_filter_test",
"filter": "duration > & < 60", # Invalid syntax with consecutive operators
"cli": "",
"extras": {}
}
with pytest.raises(ValueError, match="Invalid filter"):
conditions.validate(invalid_condition)
def test_validate_invalid_cli(self):
"""Test validating condition with invalid CLI options."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_cli.json"
conditions = Conditions(file=file_path)
invalid_condition = {
"id": str(uuid.uuid4()),
"name": "invalid_cli_test",
"filter": "duration > 60",
"cli": "--invalid-option-that-does-not-exist"
}
with pytest.raises(ValueError, match="Invalid command options"):
conditions.validate(invalid_condition)
def test_validate_invalid_extras_type(self):
"""Test validating condition with non-dict extras."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_extras.json"
conditions = Conditions(file=file_path)
invalid_condition = {
"id": str(uuid.uuid4()),
"name": "invalid_extras_test",
"filter": "duration > 60",
"extras": "not a dict"
}
with pytest.raises(ValueError, match="Extras must be a dictionary"):
conditions.validate(invalid_condition)
def test_validate_invalid_item_type(self):
"""Test validating invalid item type."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_type.json"
conditions = Conditions(file=file_path)
with pytest.raises(ValueError, match=r"Unexpected.*item type"):
conditions.validate("invalid type")
def test_save_conditions(self):
"""Test saving conditions to file."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "save_test.json"
conditions = Conditions(file=file_path)
test_conditions = [
Condition(name="save_test1", filter="duration > 60"),
Condition(name="save_test2", filter="uploader = 'test'")
]
result = conditions.save(test_conditions)
assert result is conditions
assert file_path.exists()
# Verify file content
saved_data = json.loads(file_path.read_text())
assert len(saved_data) == 2
assert saved_data[0]["name"] == "save_test1"
assert saved_data[1]["name"] == "save_test2"
def test_save_conditions_dict_format(self):
"""Test saving conditions in dictionary format."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "save_dict_test.json"
conditions = Conditions(file=file_path)
test_conditions = [
{
"id": str(uuid.uuid4()),
"name": "dict_test",
"filter": "duration < 300",
"cli": "",
"extras": {}
}
]
conditions.save(test_conditions)
assert file_path.exists()
saved_data = json.loads(file_path.read_text())
assert len(saved_data) == 1
assert saved_data[0]["name"] == "dict_test"
def test_has_condition_by_id(self):
"""Test checking if condition exists by ID."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "has_test.json"
conditions = Conditions(file=file_path)
test_id = str(uuid.uuid4())
test_condition = Condition(id=test_id, name="has_test", filter="duration > 60")
conditions._items = [test_condition]
assert conditions.has(test_id) is True
assert conditions.has("nonexistent") is False
def test_has_condition_by_name(self):
"""Test checking if condition exists by name."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "has_name_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="has_name_test", filter="uploader = 'test'")
conditions._items = [test_condition]
assert conditions.has("has_name_test") is True
assert conditions.has("nonexistent_name") is False
def test_get_condition_by_id(self):
"""Test getting condition by ID."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "get_id_test.json"
conditions = Conditions(file=file_path)
test_id = str(uuid.uuid4())
test_condition = Condition(id=test_id, name="get_id_test", filter="duration > 120")
conditions._items = [test_condition]
result = conditions.get(test_id)
assert result is test_condition
assert conditions.get("nonexistent") is None
def test_get_condition_by_name(self):
"""Test getting condition by name."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "get_name_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="get_name_test", filter="title ~= 'music'")
conditions._items = [test_condition]
result = conditions.get("get_name_test")
assert result is test_condition
def test_get_condition_empty_id_or_name(self):
"""Test getting condition with empty ID or name."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "empty_get_test.json"
conditions = Conditions(file=file_path)
assert conditions.get("") is None
assert conditions.get(None) is None
@patch("app.library.conditions.match_str")
def test_match_condition_found(self, mock_match_str):
"""Test matching condition against info dict."""
mock_match_str.return_value = True
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "match_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="match_test", filter="duration > 60")
conditions._items = [test_condition]
info_dict = {"duration": 120, "title": "Test Video"}
result = conditions.match(info_dict)
assert result is test_condition
mock_match_str.assert_called_once_with("duration > 60", info_dict)
@patch("app.library.conditions.match_str")
def test_match_condition_not_found(self, mock_match_str):
"""Test matching when no condition matches."""
mock_match_str.return_value = False
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_match_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="no_match_test", filter="duration > 300")
conditions._items = [test_condition]
info_dict = {"duration": 60, "title": "Short Video"}
result = conditions.match(info_dict)
assert result is None
def test_match_empty_conditions(self):
"""Test matching with empty conditions list."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "empty_match_test.json"
conditions = Conditions(file=file_path)
info_dict = {"duration": 120}
result = conditions.match(info_dict)
assert result is None
def test_match_invalid_info_dict(self):
"""Test matching with invalid info dict."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_info_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="test", filter="duration > 60")
conditions._items = [test_condition]
# Test with None
assert conditions.match(None) is None
# Test with empty dict
assert conditions.match({}) is None
# Test with non-dict
assert conditions.match("not a dict") is None
@patch("app.library.conditions.match_str")
def test_match_filter_evaluation_error(self, mock_match_str):
"""Test matching when filter evaluation raises exception."""
mock_match_str.side_effect = Exception("Filter error")
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "filter_error_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="error_test", filter="invalid filter")
conditions._items = [test_condition]
info_dict = {"duration": 120}
result = conditions.match(info_dict)
assert result is None
def test_match_empty_filter(self):
"""Test matching with condition that has empty filter."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "empty_filter_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="empty_filter", filter="")
conditions._items = [test_condition]
info_dict = {"duration": 120}
result = conditions.match(info_dict)
assert result is None
@patch("app.library.conditions.match_str")
def test_single_match_found(self, mock_match_str):
"""Test single condition matching."""
mock_match_str.return_value = True
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "single_match_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="single_test", filter="uploader = 'test'")
conditions._items = [test_condition]
info_dict = {"uploader": "test", "title": "Test Video"}
result = conditions.single_match("single_test", info_dict)
assert result is test_condition
mock_match_str.assert_called_once_with("uploader = 'test'", info_dict)
@patch("app.library.conditions.match_str")
def test_single_match_not_found(self, mock_match_str):
"""Test single condition matching when condition doesn't match."""
mock_match_str.return_value = False
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "single_no_match_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="single_no_match", filter="uploader = 'other'")
conditions._items = [test_condition]
info_dict = {"uploader": "test", "title": "Test Video"}
result = conditions.single_match("single_no_match", info_dict)
assert result is None
def test_single_match_nonexistent_condition(self):
"""Test single matching with non-existent condition name."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "nonexistent_single_test.json"
conditions = Conditions(file=file_path)
info_dict = {"duration": 120}
result = conditions.single_match("nonexistent", info_dict)
assert result is None
def test_single_match_condition_no_filter(self):
"""Test single matching with condition that has no filter."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "no_filter_single_test.json"
conditions = Conditions(file=file_path)
test_condition = Condition(name="no_filter_single", filter="")
conditions._items = [test_condition]
info_dict = {"duration": 120}
result = conditions.single_match("no_filter_single", info_dict)
assert result is None
def test_single_match_invalid_inputs(self):
"""Test single matching with invalid inputs."""
with tempfile.TemporaryDirectory() as temp_dir:
file_path = Path(temp_dir) / "invalid_single_test.json"
conditions = Conditions(file=file_path)
# Test with empty conditions
assert conditions.single_match("test", {"duration": 120}) is None
# Test with None info
assert conditions.single_match("test", None) is None
# Test with empty info dict
assert conditions.single_match("test", {}) is None
# Test with non-dict info
assert conditions.single_match("test", "not a dict") is None