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

625 lines
21 KiB
Python

"""
Tests for dl_fields.py - Download fields management.
This test suite provides comprehensive coverage for the dl_fields module:
- Tests FieldType enum functionality
- Tests DLField dataclass functionality
- Tests DLFields singleton class behavior
- Tests field loading, saving, and validation
- Tests field CRUD operations
- Tests error handling and edge cases
Total test functions: 25+
All dl_fields 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.dl_fields import DLField, DLFields, FieldType
class TestFieldType:
"""Test the FieldType enum."""
def test_field_type_values(self):
"""Test FieldType enum values."""
assert FieldType.STRING == "string"
assert FieldType.TEXT == "text"
assert FieldType.BOOL == "bool"
def test_field_type_all(self):
"""Test FieldType.all() method."""
all_types = FieldType.all()
expected = ["string", "text", "bool"]
assert all_types == expected
assert len(all_types) == 3
def test_field_type_from_value_valid(self):
"""Test FieldType.from_value() with valid values."""
assert FieldType.from_value("string") == FieldType.STRING
assert FieldType.from_value("text") == FieldType.TEXT
assert FieldType.from_value("bool") == FieldType.BOOL
def test_field_type_from_value_invalid(self):
"""Test FieldType.from_value() with invalid values."""
with pytest.raises(ValueError, match="Invalid StoreType value"):
FieldType.from_value("invalid")
with pytest.raises(ValueError, match="Invalid StoreType value"):
FieldType.from_value("number")
with pytest.raises(ValueError, match="Invalid StoreType value"):
FieldType.from_value("")
def test_field_type_str(self):
"""Test FieldType string conversion."""
assert str(FieldType.STRING) == "string"
assert str(FieldType.TEXT) == "text"
assert str(FieldType.BOOL) == "bool"
class TestDLField:
"""Test the DLField dataclass."""
def test_dl_field_creation_with_defaults(self):
"""Test creating a DLField with default values."""
field = DLField(name="test_field", description="Test description", field="--test-option")
# Check that ID is generated
assert field.id
assert isinstance(field.id, str)
# Check required fields
assert field.name == "test_field"
assert field.description == "Test description"
assert field.field == "--test-option"
# Check defaults
assert field.kind == FieldType.TEXT
assert field.icon == ""
assert field.order == 0
assert field.value == ""
assert field.extras == {}
def test_dl_field_creation_with_all_fields(self):
"""Test creating a DLField with all fields specified."""
test_id = str(uuid.uuid4())
extras = {"key": "value", "number": 42}
field = DLField(
id=test_id,
name="custom_field",
description="Custom description",
field="--custom-option",
kind=FieldType.BOOL,
icon="fa-check",
order=5,
value="default_value",
extras=extras,
)
assert field.id == test_id
assert field.name == "custom_field"
assert field.description == "Custom description"
assert field.field == "--custom-option"
assert field.kind == FieldType.BOOL
assert field.icon == "fa-check"
assert field.order == 5
assert field.value == "default_value"
assert field.extras == extras
def test_dl_field_serialize(self):
"""Test DLField serialization."""
extras = {"key": "value", "none_value": None}
field = DLField(name="test", description="Test field", field="--test", kind=FieldType.STRING, extras=extras)
serialized = field.serialize()
assert serialized["name"] == "test"
assert serialized["description"] == "Test field"
assert serialized["field"] == "--test"
assert serialized["kind"] == "string"
assert serialized["extras"] == {"key": "value"} # None values filtered out
def test_dl_field_json(self):
"""Test DLField JSON encoding."""
field = DLField(name="json_test", description="JSON test field", field="--json-test")
json_str = field.json()
assert isinstance(json_str, str)
# Should be valid JSON
parsed = json.loads(json_str)
assert parsed["name"] == "json_test"
assert parsed["description"] == "JSON test field"
assert parsed["field"] == "--json-test"
def test_dl_field_get_method(self):
"""Test DLField get method."""
field = DLField(name="get_test", description="Get test field", field="--get-test", order=3)
assert field.get("name") == "get_test"
assert field.get("order") == 3
assert field.get("nonexistent") is None
assert field.get("nonexistent", "default") == "default"
class TestDLFields:
"""Test the DLFields class."""
def setup_method(self):
"""Set up test fixtures."""
DLFields._reset_singleton()
@pytest.fixture
def temp_file(self):
"""Create a temporary file for testing."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
temp_path = Path(f.name)
yield temp_path
if temp_path.exists():
temp_path.unlink()
@pytest.fixture
def sample_fields_data(self):
"""Sample field data for testing."""
return [
{
"id": str(uuid.uuid4()),
"name": "quality",
"description": "Video quality setting",
"field": "--format",
"kind": "string",
"icon": "fa-video",
"order": 1,
"value": "best",
"extras": {"options": ["best", "worst", "720p"]},
},
{
"id": str(uuid.uuid4()),
"name": "audio_only",
"description": "Extract audio only",
"field": "--extract-audio",
"kind": "bool",
"icon": "fa-music",
"order": 2,
"value": "",
"extras": {},
},
]
@patch("app.library.dl_fields.Config")
def test_dl_fields_singleton_behavior(self, mock_config):
"""Test that DLFields follows singleton pattern."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields1 = DLFields()
fields2 = DLFields()
assert fields1 is fields2
@patch("app.library.dl_fields.Config")
def test_dl_fields_get_instance(self, mock_config):
"""Test DLFields.get_instance() method."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields1 = DLFields.get_instance()
fields2 = DLFields.get_instance()
assert fields1 is fields2
@patch("app.library.dl_fields.Config")
def test_dl_fields_initialization(self, mock_config):
"""Test DLFields initialization."""
mock_config.get_instance.return_value.config_path = "/tmp"
with tempfile.TemporaryDirectory() as temp_dir:
temp_file = Path(temp_dir) / "test_fields.json"
fields = DLFields(file=str(temp_file))
assert fields._file == temp_file
@patch("app.library.dl_fields.Config")
def test_dl_fields_load_empty_file(self, mock_config, temp_file):
"""Test loading from empty/non-existent file."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields(file=str(temp_file))
result = fields.load()
assert result is fields
assert fields.get_all() == []
@patch("app.library.dl_fields.Config")
def test_dl_fields_load_valid_file(self, mock_config, temp_file, sample_fields_data):
"""Test loading from valid JSON file."""
mock_config.get_instance.return_value.config_path = "/tmp"
# Write sample data to temp file
temp_file.write_text(json.dumps(sample_fields_data, indent=2))
fields = DLFields(file=str(temp_file))
fields.load()
loaded_fields = fields.get_all()
assert len(loaded_fields) == 2
# Check first field
field1 = loaded_fields[0]
assert field1.name == "quality"
assert field1.description == "Video quality setting"
assert field1.field == "--format"
assert field1.kind == FieldType.STRING
# Check second field
field2 = loaded_fields[1]
assert field2.name == "audio_only"
assert field2.kind == FieldType.BOOL
@patch("app.library.dl_fields.Config")
def test_dl_fields_load_invalid_json(self, mock_config, temp_file):
"""Test loading from invalid JSON file."""
mock_config.get_instance.return_value.config_path = "/tmp"
# Write invalid JSON
temp_file.write_text("invalid json content")
fields = DLFields(file=str(temp_file))
fields.load()
# Should handle error gracefully and return empty list
assert fields.get_all() == []
@patch("app.library.dl_fields.Config")
def test_dl_fields_load_missing_id_auto_generation(self, mock_config, temp_file):
"""Test that missing IDs are auto-generated during load."""
mock_config.get_instance.return_value.config_path = "/tmp"
# Sample data without IDs
data_without_ids = [
{"name": "test_field", "description": "Test description", "field": "--test", "kind": "string"}
]
temp_file.write_text(json.dumps(data_without_ids))
with patch.object(DLFields, "save") as mock_save:
fields = DLFields(file=str(temp_file))
fields.load()
# Should auto-generate ID and trigger save
loaded_fields = fields.get_all()
assert len(loaded_fields) == 1
assert loaded_fields[0].id is not None
mock_save.assert_called_once()
@patch("app.library.dl_fields.Config")
def test_dl_fields_clear(self, mock_config):
"""Test clearing all fields."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
# Add some fields manually
fields._items = [
DLField(name="test1", description="Test 1", field="--test1"),
DLField(name="test2", description="Test 2", field="--test2"),
]
assert len(fields.get_all()) == 2
result = fields.clear()
assert result is fields
assert len(fields.get_all()) == 0
@patch("app.library.dl_fields.Config")
def test_dl_fields_clear_empty(self, mock_config):
"""Test clearing when already empty."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
assert len(fields.get_all()) == 0
result = fields.clear()
assert result is fields
assert len(fields.get_all()) == 0
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_valid_field(self, mock_config):
"""Test validation with valid DLField."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
valid_field = DLField(
name="valid_field", description="Valid description", field="--valid-option", kind=FieldType.STRING
)
assert fields.validate(valid_field) is True
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_valid_dict(self, mock_config):
"""Test validation with valid dictionary."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
valid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "--test-field",
"kind": "text",
}
assert fields.validate(valid_dict) is True
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_missing_required_fields(self, mock_config):
"""Test validation with missing required fields."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
# Missing name
invalid_dict = {
"id": str(uuid.uuid4()),
"description": "Test description",
"field": "--test-field",
"kind": "text",
}
with pytest.raises(ValueError, match="Missing required key 'name'"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_invalid_field_type(self, mock_config):
"""Test validation with invalid field type."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
invalid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "--test-field",
"kind": "invalid_type",
}
with pytest.raises(ValueError, match="Invalid field type"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_invalid_yt_dlp_field(self, mock_config):
"""Test validation with invalid yt-dlp field format."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
invalid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "invalid-field", # Missing --
"kind": "text",
}
with pytest.raises(ValueError, match="Invalid yt-dlp option field"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_invalid_extras_type(self, mock_config):
"""Test validation with invalid extras type."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
invalid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "--test-field",
"kind": "text",
"extras": "not_a_dict",
}
with pytest.raises(ValueError, match="Extras must be a dictionary"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_invalid_value_type(self, mock_config):
"""Test validation with invalid value type."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
invalid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "--test-field",
"kind": "text",
"value": 123, # Should be string
}
with pytest.raises(ValueError, match="Value must be a string"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_invalid_order_type(self, mock_config):
"""Test validation with invalid order type."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
invalid_dict = {
"id": str(uuid.uuid4()),
"name": "test_field",
"description": "Test description",
"field": "--test-field",
"kind": "text",
"order": "not_an_int",
}
with pytest.raises(ValueError, match="Order must be an integer"):
fields.validate(invalid_dict)
@patch("app.library.dl_fields.Config")
def test_dl_fields_validate_unexpected_type(self, mock_config):
"""Test validation with unexpected item type."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
with pytest.raises(ValueError, match="Unexpected 'str' type was given"):
fields.validate("not_a_field_or_dict")
@patch("app.library.dl_fields.Config")
def test_dl_fields_save_valid_fields(self, mock_config, temp_file):
"""Test saving valid fields."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields(file=str(temp_file))
test_fields = [DLField(name="test_field", description="Test description", field="--test-option")]
fields.save(test_fields)
# Verify file was written
assert temp_file.exists()
# Verify content
saved_data = json.loads(temp_file.read_text())
assert len(saved_data) == 1
assert saved_data[0]["name"] == "test_field"
@patch("app.library.dl_fields.Config")
def test_dl_fields_save_mixed_types(self, mock_config, temp_file):
"""Test saving mix of DLField objects and dicts."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields(file=str(temp_file))
test_items = [
DLField(name="field1", description="Field 1", field="--field1"),
{"id": str(uuid.uuid4()), "name": "field2", "description": "Field 2", "field": "--field2", "kind": "text"},
]
fields.save(test_items)
# Verify both items were saved
saved_data = json.loads(temp_file.read_text())
assert len(saved_data) == 2
@patch("app.library.dl_fields.Config")
def test_dl_fields_get_by_id(self, mock_config):
"""Test getting field by ID."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
test_id = str(uuid.uuid4())
test_field = DLField(id=test_id, name="test_field", description="Test description", field="--test-option")
fields._items = [test_field]
result = fields.get(test_id)
assert result is test_field
@patch("app.library.dl_fields.Config")
def test_dl_fields_get_by_name(self, mock_config):
"""Test getting field by name."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
test_field = DLField(name="test_field", description="Test description", field="--test-option")
fields._items = [test_field]
result = fields.get("test_field")
assert result is test_field
@patch("app.library.dl_fields.Config")
def test_dl_fields_get_not_found(self, mock_config):
"""Test getting non-existent field."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
result = fields.get("nonexistent")
assert result is None
@patch("app.library.dl_fields.Config")
def test_dl_fields_get_empty_string(self, mock_config):
"""Test getting with empty string."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
result = fields.get("")
assert result is None
@patch("app.library.dl_fields.Config")
def test_dl_fields_has_by_id(self, mock_config):
"""Test checking field existence by ID."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
test_id = str(uuid.uuid4())
test_field = DLField(id=test_id, name="test_field", description="Test description", field="--test-option")
fields._items = [test_field]
assert fields.has(test_id) is True
assert fields.has("nonexistent") is False
@patch("app.library.dl_fields.Config")
def test_dl_fields_has_by_name(self, mock_config):
"""Test checking field existence by name."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
test_field = DLField(name="test_field", description="Test description", field="--test-option")
fields._items = [test_field]
assert fields.has("test_field") is True
assert fields.has("nonexistent") is False
@patch("app.library.dl_fields.Config")
def test_dl_fields_attach_method(self, mock_config):
"""Test attach method calls load."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
with patch.object(fields, "load") as mock_load:
mock_app = MagicMock()
fields.attach(mock_app)
mock_load.assert_called_once()
@patch("app.library.dl_fields.Config")
def test_dl_fields_on_shutdown(self, mock_config):
"""Test on_shutdown method."""
mock_config.get_instance.return_value.config_path = "/tmp"
fields = DLFields()
mock_app = MagicMock()
# Should not raise any exceptions
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(fields.on_shutdown(mock_app))
finally:
loop.close()