generated from unai/python_boilerplate
1251 lines
51 KiB
Python
1251 lines
51 KiB
Python
"""Tests unitarios para el módulo playlist."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
|
|
class TestPlaylistManager:
|
|
"""Tests para la clase PlaylistManager."""
|
|
|
|
@pytest.fixture
|
|
def manager(self):
|
|
"""Fixture para crear un PlaylistManager con settings mockeadas."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
mock_settings.update_interval = 1
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
yield PlaylistManager()
|
|
|
|
@pytest.fixture
|
|
def temp_dir(self):
|
|
"""Fixture para directorio temporal."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
yield Path(tmpdir)
|
|
|
|
# ========================================
|
|
# Tests para __init__
|
|
# ========================================
|
|
|
|
def test_init_running_is_false(self, manager):
|
|
"""Test: PlaylistManager inicia con running=False."""
|
|
assert manager.running is False
|
|
|
|
# ========================================
|
|
# Tests para fetch_and_generate - Complejidad Alta
|
|
# Path 1: Éxito completo
|
|
# Path 2: Error de red (RequestException)
|
|
# Path 3: Error inesperado (Exception genérica)
|
|
# Path 4: Error en JSON parsing
|
|
# ========================================
|
|
|
|
def test_fetch_and_generate_success(self, sample_channels):
|
|
"""Test: fetch_and_generate exitoso descarga y genera M3U."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = sample_channels
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
with patch.object(manager, "_write_m3u") as mock_write:
|
|
with patch.object(manager, "fetch_and_write_epg"):
|
|
manager.fetch_and_generate()
|
|
|
|
mock_get.assert_called_once()
|
|
mock_write.assert_called_once_with(sample_channels)
|
|
|
|
def test_fetch_and_generate_correct_api_call(self):
|
|
"""Test: fetch_and_generate llama a la API correctamente."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = []
|
|
mock_get.return_value = mock_response
|
|
|
|
with patch.object(manager, "_write_m3u"):
|
|
with patch.object(manager, "fetch_and_write_epg"):
|
|
manager.fetch_and_generate()
|
|
|
|
mock_get.assert_called_once_with(
|
|
"http://test-iptv.com/player_api.php",
|
|
params={
|
|
"username": "testuser",
|
|
"password": "testpass",
|
|
"action": "get_live_streams",
|
|
},
|
|
timeout=30,
|
|
)
|
|
|
|
def test_fetch_and_generate_request_exception(self, caplog):
|
|
"""Test: fetch_and_generate maneja RequestException sin crash."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_get.side_effect = requests.RequestException("Connection failed")
|
|
|
|
# No debe lanzar excepción
|
|
manager.fetch_and_generate()
|
|
|
|
assert "Error de red" in caplog.text
|
|
|
|
def test_fetch_and_generate_timeout_exception(self, caplog):
|
|
"""Test: fetch_and_generate maneja timeout sin crash."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_get.side_effect = requests.Timeout("Timeout")
|
|
|
|
manager.fetch_and_generate()
|
|
|
|
assert "Error de red" in caplog.text
|
|
|
|
def test_fetch_and_generate_http_error(self, caplog):
|
|
"""Test: fetch_and_generate maneja HTTP error (raise_for_status)."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError(
|
|
"404 Not Found"
|
|
)
|
|
mock_get.return_value = mock_response
|
|
|
|
manager.fetch_and_generate()
|
|
|
|
assert "Error de red" in caplog.text
|
|
|
|
def test_fetch_and_generate_json_decode_error(self, caplog):
|
|
"""Test: fetch_and_generate maneja error de JSON parsing."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_response.json.side_effect = ValueError("Invalid JSON")
|
|
mock_get.return_value = mock_response
|
|
|
|
manager.fetch_and_generate()
|
|
|
|
assert "Error inesperado" in caplog.text
|
|
|
|
def test_fetch_and_generate_unexpected_exception(self, caplog):
|
|
"""Test: fetch_and_generate maneja excepciones inesperadas."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_response.json.return_value = []
|
|
mock_get.return_value = mock_response
|
|
|
|
with patch.object(
|
|
manager, "_write_m3u", side_effect=RuntimeError("Disk full")
|
|
):
|
|
manager.fetch_and_generate()
|
|
|
|
assert "Error inesperado" in caplog.text
|
|
|
|
# ========================================
|
|
# Tests para _write_m3u - Complejidad Media
|
|
# Path 1: Lista vacía
|
|
# Path 2: Lista con múltiples canales
|
|
# Path 3: Canal con campos faltantes (usa defaults)
|
|
# ========================================
|
|
|
|
def test_write_m3u_empty_channels(self, temp_dir):
|
|
"""Test: _write_m3u genera archivo con solo header para lista vacía."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u([])
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
assert content == "#EXTM3U\n"
|
|
|
|
def test_write_m3u_multiple_channels(self, temp_dir, sample_channels):
|
|
"""Test: _write_m3u genera archivo correcto con múltiples canales."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(sample_channels)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Verificar header
|
|
assert content.startswith("#EXTM3U\n")
|
|
|
|
# Verificar cada canal
|
|
for channel in sample_channels:
|
|
assert channel["name"] in content
|
|
assert str(channel["stream_id"]) in content
|
|
|
|
def test_write_m3u_channel_format(self, temp_dir):
|
|
"""Test: _write_m3u genera formato EXTINF correcto."""
|
|
channel = [
|
|
{
|
|
"name": "Test Channel",
|
|
"stream_id": 123,
|
|
"stream_icon": "http://icon.com/test.png",
|
|
"category_id": "5",
|
|
}
|
|
]
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://iptv.com"
|
|
mock_settings.username = "user"
|
|
mock_settings.password = "pass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channel)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
lines = content.strip().split("\n")
|
|
|
|
assert len(lines) == 3 # Header + EXTINF + URL
|
|
|
|
# Verificar EXTINF
|
|
assert '#EXTINF:-1 tvg-id="Test Channel"' in lines[1]
|
|
assert 'tvg-logo="http://icon.com/test.png"' in lines[1]
|
|
assert 'group-title="Cat_5"' in lines[1]
|
|
assert lines[1].endswith(",Test Channel")
|
|
|
|
# Verificar URL
|
|
assert lines[2] == "http://iptv.com/live/user/pass/123.ts"
|
|
|
|
def test_write_m3u_missing_fields_uses_defaults(self, temp_dir, minimal_channel):
|
|
"""Test: _write_m3u usa valores por defecto para campos faltantes."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(minimal_channel)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Debe usar "Unknown" como nombre por defecto
|
|
assert "Unknown" in content
|
|
# Debe tener icono vacío
|
|
assert 'tvg-logo=""' in content
|
|
|
|
def test_write_m3u_atomic_replacement(self, temp_dir):
|
|
"""Test: _write_m3u reemplaza atómicamente (usa archivo temporal)."""
|
|
output_file = temp_dir / "playlist.m3u"
|
|
temp_file = temp_dir / "playlist.m3u.tmp"
|
|
|
|
# Crear archivo existente
|
|
output_file.write_text("OLD CONTENT")
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u([])
|
|
|
|
# El archivo temporal no debe existir después
|
|
assert not temp_file.exists()
|
|
|
|
# El archivo final debe tener el nuevo contenido
|
|
assert output_file.read_text() == "#EXTM3U\n"
|
|
|
|
# ========================================
|
|
# Tests para loop - Complejidad Media
|
|
# Path 1: Loop ejecuta fetch_and_generate
|
|
# Path 2: Loop respeta running=False
|
|
# ========================================
|
|
|
|
def test_loop_sets_running_true(self):
|
|
"""Test: loop establece running=True."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
mock_settings.update_interval = 1
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
# Mock fetch_and_generate para que establezca running=False
|
|
call_count = 0
|
|
|
|
def stop_after_one_call():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count >= 1:
|
|
manager.running = False
|
|
|
|
with patch.object(
|
|
manager, "fetch_and_generate", side_effect=stop_after_one_call
|
|
):
|
|
with patch("m3u_list_builder.playlist.time.sleep"):
|
|
manager.loop()
|
|
|
|
assert call_count == 1
|
|
|
|
def test_loop_calls_fetch_and_generate(self):
|
|
"""Test: loop llama a fetch_and_generate."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
mock_settings.update_interval = 1
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
fetch_mock = MagicMock(
|
|
side_effect=lambda: setattr(manager, "running", False)
|
|
)
|
|
|
|
with patch.object(manager, "fetch_and_generate", fetch_mock):
|
|
with patch("m3u_list_builder.playlist.time.sleep"):
|
|
manager.loop()
|
|
|
|
fetch_mock.assert_called()
|
|
|
|
def test_loop_respects_update_interval(self):
|
|
"""Test: loop usa el intervalo de actualización correcto."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
mock_settings.update_interval = 3600
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
call_count = 0
|
|
|
|
def stop_loop():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count >= 1:
|
|
manager.running = False
|
|
|
|
with patch.object(manager, "fetch_and_generate", side_effect=stop_loop):
|
|
with patch("m3u_list_builder.playlist.time.sleep") as mock_sleep:
|
|
manager.loop()
|
|
|
|
mock_sleep.assert_called_with(3600)
|
|
|
|
def test_loop_stops_when_running_false(self):
|
|
"""Test: loop se detiene cuando running=False."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
mock_settings.update_interval = 1
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
iterations = 0
|
|
|
|
def track_iterations():
|
|
nonlocal iterations
|
|
iterations += 1
|
|
if iterations >= 3:
|
|
manager.running = False
|
|
|
|
with patch.object(
|
|
manager, "fetch_and_generate", side_effect=track_iterations
|
|
):
|
|
with patch("m3u_list_builder.playlist.time.sleep"):
|
|
manager.loop()
|
|
|
|
assert iterations == 3
|
|
|
|
# ========================================
|
|
# Tests para filtros include_text y exclude_text
|
|
# Complejidad Alta - Múltiples paths de filtrado
|
|
# ========================================
|
|
|
|
def test_write_m3u_include_text_filters_by_prefix(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include_text filtra canales que EMPIEZAN con el texto."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["ESPN"]
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "ESPN Sports" empieza con "ESPN" - DEBE incluirse
|
|
assert "ESPN Sports" in content
|
|
# "Sports Center ESPN" NO empieza con "ESPN" - NO debe incluirse
|
|
assert "Sports Center ESPN" not in content
|
|
# Otros canales no deben estar
|
|
assert "HBO Movies" not in content
|
|
assert "CNN News" not in content
|
|
|
|
def test_write_m3u_include_text_case_insensitive(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include_text es case-insensitive."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["espn"] # minúsculas
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "ESPN Sports" debe incluirse aunque el filtro está en minúsculas
|
|
assert "ESPN Sports" in content
|
|
|
|
def test_write_m3u_include_text_multiple_prefixes(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include_text acepta múltiples prefijos (OR logic)."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["ESPN", "CNN", "BBC"]
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Canales que empiezan con ESPN, CNN o BBC
|
|
assert "ESPN Sports" in content
|
|
assert "CNN News" in content
|
|
assert "BBC World" in content
|
|
# Canales que no empiezan con ninguno
|
|
assert "HBO Movies" not in content
|
|
assert "FOX News" not in content
|
|
|
|
def test_write_m3u_include_text_empty_includes_all(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include_text vacío incluye todos los canales."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Todos los canales deben estar incluidos
|
|
for channel in channels_for_filtering:
|
|
assert channel["name"] in content
|
|
|
|
def test_write_m3u_exclude_text_filters_by_contains(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: exclude_text filtra canales que CONTIENEN el texto."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = ["Adult"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "Adult Content" contiene "Adult" - NO debe incluirse
|
|
assert "Adult Content" not in content
|
|
# Otros canales deben estar
|
|
assert "ESPN Sports" in content
|
|
assert "HBO Movies" in content
|
|
|
|
def test_write_m3u_exclude_text_matches_anywhere_in_name(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: exclude_text busca en cualquier parte del nombre."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = ["News"] # Está en medio/final
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "CNN News" y "FOX News" contienen "News" - NO deben incluirse
|
|
assert "CNN News" not in content
|
|
assert "FOX News" not in content
|
|
# Otros canales deben estar
|
|
assert "ESPN Sports" in content
|
|
assert "HBO Movies" in content
|
|
|
|
def test_write_m3u_exclude_text_case_insensitive(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: exclude_text es case-insensitive."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = ["adult"] # minúsculas
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "Adult Content" debe excluirse aunque el filtro está en minúsculas
|
|
assert "Adult Content" not in content
|
|
|
|
def test_write_m3u_exclude_text_multiple_patterns(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: exclude_text acepta múltiples patrones (OR logic)."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = ["Adult", "News"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Canales que contienen "Adult" o "News" no deben incluirse
|
|
assert "Adult Content" not in content
|
|
assert "CNN News" not in content
|
|
assert "FOX News" not in content
|
|
# Otros canales deben estar
|
|
assert "ESPN Sports" in content
|
|
assert "HBO Movies" in content
|
|
assert "BBC World" in content
|
|
|
|
def test_write_m3u_include_and_exclude_combined(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include_text y exclude_text funcionan juntos."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
# Solo canales que empiezan con "ESPN" o "Sports"
|
|
mock_settings.include_text = ["ESPN", "Sports"]
|
|
# Pero excluir los que contienen "Center"
|
|
mock_settings.exclude_text = ["Center"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "ESPN Sports" empieza con "ESPN" y no contiene "Center" - INCLUIR
|
|
assert "ESPN Sports" in content
|
|
# "Sports Center ESPN" empieza con "Sports" pero contiene "Center" - EXCLUIR
|
|
assert "Sports Center ESPN" not in content
|
|
|
|
def test_write_m3u_include_filters_before_exclude(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: include se aplica antes que exclude."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["CNN"]
|
|
mock_settings.exclude_text = ["News"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# "CNN News" empieza con "CNN" pero contiene "News" - EXCLUIR
|
|
assert "CNN News" not in content
|
|
# Solo debe quedar el header
|
|
assert content.strip() == "#EXTM3U"
|
|
|
|
def test_write_m3u_no_match_for_include_results_in_empty(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: si ningún canal coincide con include, resultado vacío."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["NONEXISTENT"]
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Solo header, sin canales
|
|
assert content == "#EXTM3U\n"
|
|
|
|
def test_write_m3u_exclude_all_results_in_empty(
|
|
self, temp_dir, channels_for_filtering
|
|
):
|
|
"""Test: si todos los canales son excluidos, resultado vacío."""
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = []
|
|
# Excluir texto común a todos
|
|
mock_settings.exclude_text = ["a", "e", "i", "o", "u"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels_for_filtering)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Solo header, sin canales (todos tienen vocales)
|
|
assert content == "#EXTM3U\n"
|
|
|
|
def test_write_m3u_include_startswith_not_contains(self, temp_dir):
|
|
"""Test: include usa startswith, NO contains - verificación explícita."""
|
|
channels = [
|
|
{"name": "ABC News", "stream_id": 1, "stream_icon": "", "category_id": "1"},
|
|
{
|
|
"name": "News ABC",
|
|
"stream_id": 2,
|
|
"stream_icon": "",
|
|
"category_id": "1",
|
|
}, # Contiene ABC pero no empieza
|
|
{
|
|
"name": "ZABC Channel",
|
|
"stream_id": 3,
|
|
"stream_icon": "",
|
|
"category_id": "1",
|
|
}, # Contiene ABC pero no empieza
|
|
]
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "playlist.m3u"
|
|
mock_settings.include_text = ["ABC"]
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
manager._write_m3u(channels)
|
|
|
|
output_file = temp_dir / "playlist.m3u"
|
|
content = output_file.read_text()
|
|
|
|
# Solo "ABC News" debe estar - empieza con ABC
|
|
assert "ABC News" in content
|
|
# "News ABC" y "ZABC Channel" NO deben estar
|
|
assert "News ABC" not in content
|
|
assert "ZABC Channel" not in content
|
|
|
|
# ========================================
|
|
# Tests para fetch_and_write_epg - EPG Functionality
|
|
# ========================================
|
|
|
|
def test_fetch_and_write_epg_success(self, temp_dir):
|
|
"""Test: fetch_and_write_epg descarga y guarda EPG correctamente."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>ESPN Sports</display-name>
|
|
</channel>
|
|
<programme channel="ch1" start="20260203" stop="20260203">
|
|
<title>Sports Event</title>
|
|
</programme>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.content = sample_epg
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
manager.fetch_and_write_epg()
|
|
|
|
# Verify EPG file was written
|
|
epg_file = temp_dir / "epg.xml"
|
|
assert epg_file.exists()
|
|
content = epg_file.read_text()
|
|
assert "ESPN Sports" in content
|
|
|
|
def test_fetch_and_write_epg_correct_api_call(self, temp_dir):
|
|
"""Test: fetch_and_write_epg llama a la API correctamente."""
|
|
sample_epg = b'<?xml version="1.0"?><tv></tv>'
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.content = sample_epg
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
manager.fetch_and_write_epg()
|
|
|
|
mock_get.assert_called_once_with(
|
|
"http://test-iptv.com/xmltv.php",
|
|
params={
|
|
"username": "testuser",
|
|
"password": "testpass",
|
|
},
|
|
timeout=60,
|
|
)
|
|
|
|
def test_fetch_and_write_epg_request_exception(self, caplog):
|
|
"""Test: fetch_and_write_epg maneja RequestException sin crash."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_get.side_effect = requests.RequestException("Connection failed")
|
|
|
|
manager.fetch_and_write_epg()
|
|
|
|
assert "Error de red al obtener EPG" in caplog.text
|
|
|
|
def test_fetch_and_write_epg_parse_error(self, caplog):
|
|
"""Test: fetch_and_write_epg maneja error de parsing XML."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.content = b"invalid xml <not closed"
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
manager.fetch_and_write_epg()
|
|
|
|
assert "Error parseando XML de EPG" in caplog.text
|
|
|
|
def test_fetch_and_write_epg_unexpected_exception(self, caplog):
|
|
"""Test: fetch_and_write_epg maneja excepciones inesperadas."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.content = b'<?xml version="1.0"?><tv></tv>'
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
with patch.object(
|
|
manager, "_write_epg", side_effect=RuntimeError("Disk full")
|
|
):
|
|
manager.fetch_and_write_epg()
|
|
|
|
assert "Error inesperado actualizando EPG" in caplog.text
|
|
|
|
# ========================================
|
|
# Tests para _filter_epg - Filtrado de EPG
|
|
# ========================================
|
|
|
|
def test_filter_epg_include_text_filters_by_prefix(self):
|
|
"""Test: _filter_epg filtra canales que EMPIEZAN con el texto."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>ESPN Sports</display-name>
|
|
</channel>
|
|
<channel id="ch2">
|
|
<display-name>Sports Center ESPN</display-name>
|
|
</channel>
|
|
<channel id="ch3">
|
|
<display-name>HBO Movies</display-name>
|
|
</channel>
|
|
<programme channel="ch1" start="20260203" stop="20260203">
|
|
<title>Sports Event</title>
|
|
</programme>
|
|
<programme channel="ch2" start="20260203" stop="20260203">
|
|
<title>Center Show</title>
|
|
</programme>
|
|
<programme channel="ch3" start="20260203" stop="20260203">
|
|
<title>Movie</title>
|
|
</programme>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = ["ESPN"]
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
import xml.etree.ElementTree as ET
|
|
|
|
manager = PlaylistManager()
|
|
result = manager._filter_epg(sample_epg)
|
|
|
|
# Convert to string for easier assertion
|
|
result_str = ET.tostring(result, encoding="unicode")
|
|
|
|
# ESPN Sports starts with ESPN - should be included
|
|
assert "ESPN Sports" in result_str
|
|
# Sports Center ESPN does NOT start with ESPN - should be filtered
|
|
assert "Sports Center ESPN" not in result_str
|
|
# HBO Movies - should be filtered
|
|
assert "HBO Movies" not in result_str
|
|
# Programme for ch1 should remain
|
|
assert "Sports Event" in result_str
|
|
# Programmes for ch2 and ch3 should be removed
|
|
assert "Center Show" not in result_str
|
|
assert "Movie" not in result_str
|
|
|
|
def test_filter_epg_exclude_text_filters_by_contains(self):
|
|
"""Test: _filter_epg excluye canales que CONTIENEN el texto."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>ESPN Sports</display-name>
|
|
</channel>
|
|
<channel id="ch2">
|
|
<display-name>Adult Content</display-name>
|
|
</channel>
|
|
<programme channel="ch1" start="20260203" stop="20260203">
|
|
<title>Sports Event</title>
|
|
</programme>
|
|
<programme channel="ch2" start="20260203" stop="20260203">
|
|
<title>Adult Show</title>
|
|
</programme>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = ["Adult"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
import xml.etree.ElementTree as ET
|
|
|
|
manager = PlaylistManager()
|
|
result = manager._filter_epg(sample_epg)
|
|
|
|
result_str = ET.tostring(result, encoding="unicode")
|
|
|
|
# ESPN Sports - should remain
|
|
assert "ESPN Sports" in result_str
|
|
# Adult Content - should be filtered
|
|
assert "Adult Content" not in result_str
|
|
# Programme for ch1 should remain
|
|
assert "Sports Event" in result_str
|
|
# Programme for ch2 should be removed
|
|
assert "Adult Show" not in result_str
|
|
|
|
def test_filter_epg_no_filters_keeps_all(self):
|
|
"""Test: _filter_epg sin filtros mantiene todos los canales."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>Channel 1</display-name>
|
|
</channel>
|
|
<channel id="ch2">
|
|
<display-name>Channel 2</display-name>
|
|
</channel>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = []
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
import xml.etree.ElementTree as ET
|
|
|
|
manager = PlaylistManager()
|
|
result = manager._filter_epg(sample_epg)
|
|
|
|
result_str = ET.tostring(result, encoding="unicode")
|
|
|
|
assert "Channel 1" in result_str
|
|
assert "Channel 2" in result_str
|
|
|
|
def test_filter_epg_include_and_exclude_combined(self):
|
|
"""Test: _filter_epg aplica include y exclude juntos."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>ESPN Sports</display-name>
|
|
</channel>
|
|
<channel id="ch2">
|
|
<display-name>ESPN Adult</display-name>
|
|
</channel>
|
|
<channel id="ch3">
|
|
<display-name>HBO Movies</display-name>
|
|
</channel>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = ["ESPN"]
|
|
mock_settings.exclude_text = ["Adult"]
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
import xml.etree.ElementTree as ET
|
|
|
|
manager = PlaylistManager()
|
|
result = manager._filter_epg(sample_epg)
|
|
|
|
result_str = ET.tostring(result, encoding="unicode")
|
|
|
|
# ESPN Sports - starts with ESPN, no Adult - should remain
|
|
assert "ESPN Sports" in result_str
|
|
# ESPN Adult - starts with ESPN but contains Adult - should be filtered
|
|
assert "ESPN Adult" not in result_str
|
|
# HBO Movies - doesn't start with ESPN - should be filtered
|
|
assert "HBO Movies" not in result_str
|
|
|
|
def test_filter_epg_case_insensitive(self):
|
|
"""Test: _filter_epg es case-insensitive."""
|
|
sample_epg = b'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<tv>
|
|
<channel id="ch1">
|
|
<display-name>ESPN Sports</display-name>
|
|
</channel>
|
|
</tv>'''
|
|
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.include_text = ["espn"] # lowercase
|
|
mock_settings.exclude_text = []
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
import xml.etree.ElementTree as ET
|
|
|
|
manager = PlaylistManager()
|
|
result = manager._filter_epg(sample_epg)
|
|
|
|
result_str = ET.tostring(result, encoding="unicode")
|
|
|
|
# Should still match despite case difference
|
|
assert "ESPN Sports" in result_str
|
|
|
|
# ========================================
|
|
# Tests para _write_epg
|
|
# ========================================
|
|
|
|
def test_write_epg_creates_file(self, temp_dir):
|
|
"""Test: _write_epg crea el archivo EPG correctamente."""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
root = ET.Element("tv")
|
|
channel = ET.SubElement(root, "channel", id="ch1")
|
|
name = ET.SubElement(channel, "display-name")
|
|
name.text = "Test Channel"
|
|
|
|
manager._write_epg(root)
|
|
|
|
epg_file = temp_dir / "epg.xml"
|
|
assert epg_file.exists()
|
|
content = epg_file.read_text()
|
|
assert "Test Channel" in content
|
|
|
|
def test_write_epg_atomic_replacement(self, temp_dir):
|
|
"""Test: _write_epg reemplaza atómicamente."""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
epg_file = temp_dir / "epg.xml"
|
|
temp_file = temp_dir / "epg.xml.tmp"
|
|
|
|
# Create existing file
|
|
epg_file.write_text("OLD CONTENT")
|
|
|
|
with patch("m3u_list_builder.playlist.PUBLIC_DIR", temp_dir):
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
root = ET.Element("tv")
|
|
manager._write_epg(root)
|
|
|
|
# Temp file should not exist after
|
|
assert not temp_file.exists()
|
|
# Final file should have new content
|
|
assert "OLD CONTENT" not in epg_file.read_text()
|
|
|
|
def test_fetch_and_generate_calls_epg(self, sample_channels):
|
|
"""Test: fetch_and_generate también llama a fetch_and_write_epg."""
|
|
with patch("m3u_list_builder.playlist.settings") as mock_settings:
|
|
mock_settings.host = "http://test-iptv.com"
|
|
mock_settings.username = "testuser"
|
|
mock_settings.password = "testpass"
|
|
mock_settings.output_file = "test_playlist.m3u"
|
|
|
|
from m3u_list_builder.playlist import PlaylistManager
|
|
|
|
manager = PlaylistManager()
|
|
|
|
with patch("m3u_list_builder.playlist.requests.get") as mock_get:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = sample_channels
|
|
mock_response.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_response
|
|
|
|
with patch.object(manager, "_write_m3u"):
|
|
with patch.object(
|
|
manager, "fetch_and_write_epg"
|
|
) as mock_epg:
|
|
manager.fetch_and_generate()
|
|
|
|
mock_epg.assert_called_once()
|