"""Tests unitarios para el módulo config.""" import os import pytest from pydantic import ValidationError @pytest.fixture(autouse=True) def clean_env(monkeypatch): """Limpia todas las variables de entorno relevantes antes de cada test.""" env_vars = [ "HOST", "USERNAME", "PASSWORD", "PORT", "UPDATE_INTERVAL", "OUTPUT_FILE", ] for var in env_vars: monkeypatch.delenv(var, raising=False) yield class TestSettings: """Tests para la clase Settings.""" def test_settings_with_required_fields(self, monkeypatch): """Test: Settings se crea correctamente con campos obligatorios.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings # _env_file=None evita leer el archivo .env settings = Settings(_env_file=None) assert settings.host == "http://test.com" assert settings.username == "user" assert settings.password == "pass" def test_settings_default_values(self, monkeypatch): """Test: Settings usa valores por defecto correctos.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.port == 8080 assert settings.update_interval == 3600 assert settings.output_file == "playlist.m3u" def test_settings_custom_values(self, monkeypatch): """Test: Settings acepta valores personalizados.""" monkeypatch.setenv("HOST", "http://custom.com") monkeypatch.setenv("USERNAME", "custom_user") monkeypatch.setenv("PASSWORD", "custom_pass") monkeypatch.setenv("PORT", "9090") monkeypatch.setenv("UPDATE_INTERVAL", "7200") monkeypatch.setenv("OUTPUT_FILE", "custom.m3u") from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.host == "http://custom.com" assert settings.username == "custom_user" assert settings.password == "custom_pass" assert settings.port == 9090 assert settings.update_interval == 7200 assert settings.output_file == "custom.m3u" def test_settings_missing_required_host(self, monkeypatch): """Test: Settings falla sin HOST.""" monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings with pytest.raises(ValidationError) as exc_info: Settings(_env_file=None) assert "host" in str(exc_info.value).lower() def test_settings_missing_required_username(self, monkeypatch): """Test: Settings falla sin USERNAME.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings with pytest.raises(ValidationError) as exc_info: Settings(_env_file=None) assert "username" in str(exc_info.value).lower() def test_settings_missing_required_password(self, monkeypatch): """Test: Settings falla sin PASSWORD.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") from m3u_list_builder.config import Settings with pytest.raises(ValidationError) as exc_info: Settings(_env_file=None) assert "password" in str(exc_info.value).lower() def test_settings_invalid_port_type(self, monkeypatch): """Test: Settings falla con PORT inválido.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("PORT", "invalid") from m3u_list_builder.config import Settings with pytest.raises(ValidationError): Settings(_env_file=None) def test_settings_extra_fields_ignored(self, monkeypatch): """Test: Settings ignora campos extra (extra='ignore').""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("UNKNOWN_FIELD", "should_be_ignored") from m3u_list_builder.config import Settings # No debe lanzar excepción settings = Settings(_env_file=None) assert not hasattr(settings, "unknown_field") def test_settings_reads_env_file(self, tmp_path, monkeypatch): """Test: Settings puede leer desde archivo .env.""" # Crear archivo .env temporal env_file = tmp_path / ".env" env_file.write_text( "HOST=http://from-env-file.com\nUSERNAME=envuser\nPASSWORD=envpass\n" ) from m3u_list_builder.config import Settings settings = Settings(_env_file=str(env_file)) assert settings.host == "http://from-env-file.com" assert settings.username == "envuser" assert settings.password == "envpass" def test_settings_invalid_port_type(self, monkeypatch): """Test: Settings falla con PORT inválido.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("PORT", "invalid") from m3u_list_builder.config import Settings with pytest.raises(ValidationError): Settings() def test_settings_extra_fields_ignored(self, monkeypatch): """Test: Settings ignora campos extra (extra='ignore').""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("UNKNOWN_FIELD", "should_be_ignored") from m3u_list_builder.config import Settings # No debe lanzar excepción settings = Settings() assert not hasattr(settings, "unknown_field") # ======================================== # Tests para include_text y exclude_text # ======================================== def test_settings_include_text_default_empty_list(self, monkeypatch): """Test: include_text tiene valor por defecto lista vacía.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.include_text == [] assert isinstance(settings.include_text, list) def test_settings_exclude_text_default_empty_list(self, monkeypatch): """Test: exclude_text tiene valor por defecto lista vacía.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.exclude_text == [] assert isinstance(settings.exclude_text, list) def test_settings_include_text_from_env_json(self, monkeypatch): """Test: include_text acepta formato JSON desde variable de entorno.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("INCLUDE_TEXT", '["ESPN", "CNN"]') from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.include_text == ["ESPN", "CNN"] def test_settings_exclude_text_from_env_json(self, monkeypatch): """Test: exclude_text acepta formato JSON desde variable de entorno.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("EXCLUDE_TEXT", '["Adult", "XXX"]') from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.exclude_text == ["Adult", "XXX"] def test_settings_include_and_exclude_together(self, monkeypatch): """Test: include_text y exclude_text pueden usarse juntos.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("INCLUDE_TEXT", '["Sports", "News"]') monkeypatch.setenv("EXCLUDE_TEXT", '["Adult"]') from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.include_text == ["Sports", "News"] assert settings.exclude_text == ["Adult"] def test_settings_include_text_single_value(self, monkeypatch): """Test: include_text acepta un solo valor en lista.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("INCLUDE_TEXT", '["OnlyThis"]') from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.include_text == ["OnlyThis"] assert len(settings.include_text) == 1 def test_settings_exclude_text_single_value(self, monkeypatch): """Test: exclude_text acepta un solo valor en lista.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("EXCLUDE_TEXT", '["BlockThis"]') from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.exclude_text == ["BlockThis"] assert len(settings.exclude_text) == 1 def test_settings_include_text_empty_json_array(self, monkeypatch): """Test: include_text acepta array JSON vacío.""" monkeypatch.setenv("HOST", "http://test.com") monkeypatch.setenv("USERNAME", "user") monkeypatch.setenv("PASSWORD", "pass") monkeypatch.setenv("INCLUDE_TEXT", "[]") from m3u_list_builder.config import Settings settings = Settings(_env_file=None) assert settings.include_text == [] def test_settings_from_env_file_with_filters(self, tmp_path, monkeypatch): """Test: Settings lee filtros desde archivo .env.""" env_file = tmp_path / ".env" env_file.write_text( 'HOST=http://test.com\n' 'USERNAME=user\n' 'PASSWORD=pass\n' 'INCLUDE_TEXT=["HBO", "ESPN"]\n' 'EXCLUDE_TEXT=["Adult"]\n' ) from m3u_list_builder.config import Settings settings = Settings(_env_file=str(env_file)) assert settings.include_text == ["HBO", "ESPN"] assert settings.exclude_text == ["Adult"]