"""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