unai 128432f679
Some checks failed
CI/CD Pipeline / test-and-lint (push) Failing after 32s
CI/CD Pipeline / publish-container (push) Has been skipped
Add unit tests for all modules including config, main, playlist, and server
2026-02-01 18:47:32 +00:00

461 lines
18 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:
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"):
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."""
output_file = temp_dir / "playlist.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 = str(output_file)
from m3u_list_builder.playlist import PlaylistManager
manager = PlaylistManager()
manager._write_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."""
output_file = temp_dir / "playlist.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 = str(output_file)
from m3u_list_builder.playlist import PlaylistManager
manager = PlaylistManager()
manager._write_m3u(sample_channels)
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."""
output_file = temp_dir / "playlist.m3u"
channel = [
{
"name": "Test Channel",
"stream_id": 123,
"stream_icon": "http://icon.com/test.png",
"category_id": "5",
}
]
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 = str(output_file)
from m3u_list_builder.playlist import PlaylistManager
manager = PlaylistManager()
manager._write_m3u(channel)
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."""
output_file = temp_dir / "playlist.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 = str(output_file)
from m3u_list_builder.playlist import PlaylistManager
manager = PlaylistManager()
manager._write_m3u(minimal_channel)
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.settings") as mock_settings:
mock_settings.host = "http://test-iptv.com"
mock_settings.username = "testuser"
mock_settings.password = "testpass"
mock_settings.output_file = str(output_file)
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