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