625 lines
21 KiB
Python
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()
|