"""Tests for configuration settings.""" import pytest from meshcore_hub.common.config import ( CommonSettings, CollectorSettings, APISettings, WebSettings, ) class TestCommonSettings: """Tests for CommonSettings.""" def test_custom_data_home(self) -> None: """Test custom DATA_HOME setting.""" settings = CommonSettings(_env_file=None, data_home="/custom/data") assert settings.data_home == "/custom/data" def test_websocket_transport_settings(self) -> None: """Test MQTT websocket transport settings.""" settings = CommonSettings( _env_file=None, mqtt_transport="websockets", mqtt_ws_path="/", ) assert settings.mqtt_transport.value == "websockets" assert settings.mqtt_ws_path == "/" class TestCollectorSettings: """Tests for CollectorSettings.""" def test_custom_data_home(self) -> None: """Test that custom data_home affects effective paths.""" settings = CollectorSettings(_env_file=None, data_home="/custom/data") assert ( settings.effective_database_url == "sqlite:////custom/data/collector/meshcore.db" ) assert settings.collector_data_dir == "/custom/data/collector" def test_explicit_database_url_overrides(self) -> None: """Test that explicit database_url overrides the default.""" settings = CollectorSettings( _env_file=None, database_url="postgresql://user@host/db" ) assert settings.database_url == "postgresql://user@host/db" assert settings.effective_database_url == "postgresql://user@host/db" def test_raw_packet_retention_defaults_to_7(self) -> None: """Unset raw_packet_retention_days defaults to 7, independent of global.""" settings = CollectorSettings(_env_file=None, data_retention_days=12) assert settings.raw_packet_retention_days == 7 assert settings.effective_raw_packet_retention_days == 7 def test_raw_packet_retention_explicit_override(self) -> None: """An explicit raw_packet_retention_days wins over the global value.""" settings = CollectorSettings( _env_file=None, data_retention_days=30, raw_packet_retention_days=3 ) assert settings.effective_raw_packet_retention_days == 3 def test_raw_packet_capture_disabled_by_default(self) -> None: """raw_packet_capture_enabled defaults to False.""" settings = CollectorSettings(_env_file=None) assert settings.raw_packet_capture_enabled is False def test_observer_lists_default_empty(self) -> None: """Observer allow/deny lists default to empty (accept-all).""" settings = CollectorSettings(_env_file=None) assert settings.observer_allowlist_keys == [] assert settings.observer_denylist_keys == [] def test_observer_allowlist_parsed_and_trimmed(self) -> None: """Comma-separated allowlist is split and blank entries dropped.""" settings = CollectorSettings( _env_file=None, observer_allowlist="aaa, bbb ,, ,ccc" ) assert settings.observer_allowlist_keys == ["aaa", "bbb", "ccc"] def test_observer_denylist_parsed_and_trimmed(self) -> None: """Comma-separated denylist is split and blank entries dropped.""" settings = CollectorSettings(_env_file=None, observer_denylist=" key1 ,key2") assert settings.observer_denylist_keys == ["key1", "key2"] def test_explicit_seed_home_overrides(self) -> None: """Test that explicit seed_home overrides the default.""" settings = CollectorSettings(_env_file=None, seed_home="/seed/data") assert settings.seed_home == "/seed/data" assert settings.effective_seed_home == "/seed/data" assert settings.node_tags_file == "/seed/data/node_tags.yaml" def test_channel_refresh_interval_seconds(self) -> None: """Channel refresh interval defaults to 300.""" settings = CollectorSettings(_env_file=None) assert settings.channel_refresh_interval_seconds == 300 def test_channel_refresh_interval_seconds_custom(self) -> None: """Channel refresh interval can be overridden.""" settings = CollectorSettings( _env_file=None, channel_refresh_interval_seconds=60, ) assert settings.channel_refresh_interval_seconds == 60 def test_channels_file_path(self) -> None: """channels_file property resolves to seed_home/channels.yaml.""" settings = CollectorSettings(_env_file=None, seed_home="/seed/data") assert settings.channels_file == "/seed/data/channels.yaml" def test_channels_file_default(self) -> None: """channels_file uses default seed_home.""" settings = CollectorSettings(_env_file=None) assert settings.channels_file.endswith("channels.yaml") assert "seed" in settings.channels_file class TestAPISettings: """Tests for APISettings.""" def test_custom_data_home(self) -> None: """Test that custom data_home affects effective database path.""" settings = APISettings(_env_file=None, data_home="/custom/data") assert ( settings.effective_database_url == "sqlite:////custom/data/collector/meshcore.db" ) def test_explicit_database_url_overrides(self) -> None: """Test that explicit database_url overrides the default.""" settings = APISettings(_env_file=None, database_url="postgresql://user@host/db") assert settings.database_url == "postgresql://user@host/db" assert settings.effective_database_url == "postgresql://user@host/db" class TestWebSettings: """Tests for WebSettings.""" def test_custom_data_home(self) -> None: """Test that custom data_home affects effective paths.""" settings = WebSettings(_env_file=None, data_home="/custom/data") assert settings.web_data_dir == "/custom/data/web" def test_network_announcement_default_none(self) -> None: """Test that network_announcement defaults to None.""" settings = WebSettings(_env_file=None) assert settings.network_announcement is None def test_system_announcement_default_none(self) -> None: """Test that system_announcement defaults to None.""" settings = WebSettings(_env_file=None) assert settings.system_announcement is None def test_system_maintenance_default_false(self) -> None: """Test that system_maintenance defaults to False.""" settings = WebSettings(_env_file=None) assert settings.system_maintenance is False def test_feature_channels_default_true(self) -> None: """Test that feature_channels defaults to True.""" settings = WebSettings(_env_file=None) assert settings.feature_channels is True def test_radio_config_defaults(self) -> None: """Test that radio config fields default to EU/UK Narrow values.""" settings = WebSettings(_env_file=None) assert settings.network_radio_profile == "EU/UK Narrow" assert settings.network_radio_frequency == 869.618 assert settings.network_radio_bandwidth == 62.5 assert settings.network_radio_spreading_factor == 8 assert settings.network_radio_coding_rate == 8 assert settings.network_radio_tx_power == 22.0 def test_radio_config_no_legacy_field(self) -> None: """Test that network_radio_config no longer exists.""" settings = WebSettings(_env_file=None) assert not hasattr(settings, "network_radio_config") def test_radio_config_custom_frequency(self) -> None: """Test that frequency can be set as a float.""" settings = WebSettings( _env_file=None, network_radio_frequency=915.0, ) assert settings.network_radio_frequency == 915.0 def test_feature_channels_override(self) -> None: """Test that feature_channels can be disabled.""" settings = WebSettings(_env_file=None, feature_channels=False) assert settings.feature_channels is False def test_features_dict_includes_channels(self) -> None: """Test that features dict includes channels key.""" settings = WebSettings(_env_file=None) features = settings.features assert "channels" in features assert features["channels"] is True def test_features_dashboard_auto_disables(self) -> None: """Dashboard disables when nodes, ads, and messages all off.""" settings = WebSettings( _env_file=None, feature_dashboard=True, feature_nodes=False, feature_advertisements=False, feature_messages=False, ) assert settings.features["dashboard"] is False def test_features_map_auto_disables_without_nodes(self) -> None: """Map disables when nodes feature is off.""" settings = WebSettings( _env_file=None, feature_map=True, feature_nodes=False, ) assert settings.features["map"] is False def test_features_members_auto_disables_without_oidc(self) -> None: """Members disables when OIDC is not enabled.""" settings = WebSettings( _env_file=None, feature_members=True, oidc_enabled=False, ) assert settings.features["members"] is False def test_features_all_enabled_by_default(self) -> None: """All features are enabled with default settings.""" settings = WebSettings( _env_file=None, oidc_enabled=True, ) features = settings.features assert features["dashboard"] is True assert features["nodes"] is True assert features["advertisements"] is True assert features["messages"] is True assert features["map"] is True assert features["members"] is True assert features["channels"] is True assert features["pages"] is True assert features["radio_config"] is True def test_feature_radio_config_default_true(self) -> None: """Test that feature_radio_config defaults to True.""" settings = WebSettings(_env_file=None) assert settings.feature_radio_config is True def test_feature_radio_config_can_disable(self) -> None: """Test that feature_radio_config can be disabled.""" settings = WebSettings(_env_file=None, feature_radio_config=False) assert settings.feature_radio_config is False assert settings.features["radio_config"] is False class TestDatabaseBackendResolution: """Tests for DATABASE_BACKEND selection and URL/schema resolution.""" def test_default_backend_is_sqlite_unchanged(self) -> None: """No DB env vars -> the same SQLite path and no schema as before.""" settings = CollectorSettings(_env_file=None, data_home="/data") assert settings.database_backend.value == "sqlite" assert ( settings.effective_database_url == "sqlite:////data/collector/meshcore.db" ) assert settings.effective_database_schema is None def test_postgres_backend_assembles_url_and_schema(self) -> None: """Postgres backend assembles a URL from components and exposes the schema.""" settings = APISettings( _env_file=None, database_backend="postgres", database_host="pg", database_password="pw", ) assert settings.effective_database_url == ( "postgresql+psycopg2://meshcorehub:pw@pg:5432/meshcorehub" ) assert settings.effective_database_schema == "meshcorehub" def test_postgres_password_is_url_encoded(self) -> None: """Special characters in the password are percent-encoded.""" settings = APISettings( _env_file=None, database_backend="postgres", database_host="pg", database_password="s3cr3t/p@ss", ) assert "s3cr3t%2Fp%40ss" in settings.effective_database_url def test_postgres_schema_override_per_instance(self) -> None: """DATABASE_SCHEMA isolates instances sharing one database.""" settings = APISettings( _env_file=None, database_backend="postgres", database_host="pg", database_password="pw", database_schema="stg", ) assert settings.effective_database_schema == "stg" def test_postgres_missing_required_vars_fails_fast(self) -> None: """Misconfigured postgres backend raises rather than silently using SQLite.""" settings = APISettings(_env_file=None, database_backend="postgres") with pytest.raises(ValueError, match="DATABASE_BACKEND=postgres requires"): _ = settings.effective_database_url def test_explicit_url_overrides_backend(self) -> None: """An explicit DATABASE_URL wins even when a backend is selected.""" settings = CollectorSettings( _env_file=None, database_backend="postgres", database_url="postgresql+psycopg2://u:p@h/db", ) assert settings.effective_database_url == "postgresql+psycopg2://u:p@h/db"