"""Tests for environment variable parsing and Config class.""" import pytest from meshmon.env import ( Config, get_bool, get_config, get_int, get_str, ) class TestGetStrEdgeCases: """Additional edge case tests for get_str.""" def test_whitespace_value_preserved(self, monkeypatch): """Whitespace-only value is preserved.""" monkeypatch.setenv("TEST_VAR", " ") assert get_str("TEST_VAR") == " " def test_special_characters(self, monkeypatch): """Special characters are preserved.""" monkeypatch.setenv("TEST_VAR", "hello@world#123!") assert get_str("TEST_VAR") == "hello@world#123!" class TestGetIntEdgeCases: """Additional edge case tests for get_int.""" def test_leading_zeros(self, monkeypatch): """Leading zeros work (not octal).""" monkeypatch.setenv("TEST_INT", "042") assert get_int("TEST_INT", 0) == 42 def test_whitespace_around_number(self, monkeypatch): """Whitespace around number is tolerated by int().""" monkeypatch.setenv("TEST_INT", " 42 ") # Python's int() handles whitespace assert get_int("TEST_INT", 0) == 42 class TestGetBoolEdgeCases: """Additional edge case tests for get_bool.""" def test_mixed_case(self, monkeypatch): """Mixed case variants work.""" monkeypatch.setenv("TEST_BOOL", "TrUe") assert get_bool("TEST_BOOL") is True def test_with_spaces(self, monkeypatch): """Whitespace causes a non-match since get_bool does not strip.""" monkeypatch.setenv("TEST_BOOL", " yes ") # .lower() doesn't strip, so " yes " != "yes" # This will return False assert get_bool("TEST_BOOL") is False class TestConfigComplete: """Complete Config class tests.""" def test_all_connection_settings(self, clean_env, monkeypatch): """All connection settings are loaded.""" monkeypatch.setenv("MESH_TRANSPORT", "tcp") monkeypatch.setenv("MESH_SERIAL_PORT", "/dev/ttyUSB0") monkeypatch.setenv("MESH_SERIAL_BAUD", "9600") monkeypatch.setenv("MESH_TCP_HOST", "192.168.1.1") monkeypatch.setenv("MESH_TCP_PORT", "8080") monkeypatch.setenv("MESH_BLE_ADDR", "AA:BB:CC:DD:EE:FF") monkeypatch.setenv("MESH_BLE_PIN", "1234") monkeypatch.setenv("MESH_DEBUG", "true") config = Config() assert config.mesh_transport == "tcp" assert config.mesh_serial_port == "/dev/ttyUSB0" assert config.mesh_serial_baud == 9600 assert config.mesh_tcp_host == "192.168.1.1" assert config.mesh_tcp_port == 8080 assert config.mesh_ble_addr == "AA:BB:CC:DD:EE:FF" assert config.mesh_ble_pin == "1234" assert config.mesh_debug is True def test_all_repeater_settings(self, clean_env, monkeypatch): """All repeater identity settings are loaded.""" monkeypatch.setenv("REPEATER_NAME", "HilltopRepeater") monkeypatch.setenv("REPEATER_KEY_PREFIX", "abc123") monkeypatch.setenv("REPEATER_PASSWORD", "secret") monkeypatch.setenv("REPEATER_DISPLAY_NAME", "Hilltop Relay") monkeypatch.setenv("REPEATER_PUBKEY_PREFIX", "!abc123") monkeypatch.setenv("REPEATER_HARDWARE", "RAK4631 with Solar") config = Config() assert config.repeater_name == "HilltopRepeater" assert config.repeater_key_prefix == "abc123" assert config.repeater_password == "secret" assert config.repeater_display_name == "Hilltop Relay" assert config.repeater_pubkey_prefix == "!abc123" assert config.repeater_hardware == "RAK4631 with Solar" def test_all_timeout_settings(self, clean_env, monkeypatch): """All timeout and retry settings are loaded.""" monkeypatch.setenv("REMOTE_TIMEOUT_S", "30") monkeypatch.setenv("REMOTE_RETRY_ATTEMPTS", "5") monkeypatch.setenv("REMOTE_RETRY_BACKOFF_S", "10") monkeypatch.setenv("REMOTE_CB_FAILS", "10") monkeypatch.setenv("REMOTE_CB_COOLDOWN_S", "7200") config = Config() assert config.remote_timeout_s == 30 assert config.remote_retry_attempts == 5 assert config.remote_retry_backoff_s == 10 assert config.remote_cb_fails == 10 assert config.remote_cb_cooldown_s == 7200 def test_all_telemetry_settings(self, clean_env, monkeypatch): """All telemetry settings are loaded.""" monkeypatch.setenv("TELEMETRY_ENABLED", "yes") monkeypatch.setenv("TELEMETRY_TIMEOUT_S", "20") monkeypatch.setenv("TELEMETRY_RETRY_ATTEMPTS", "3") monkeypatch.setenv("TELEMETRY_RETRY_BACKOFF_S", "5") config = Config() assert config.telemetry_enabled is True assert config.telemetry_timeout_s == 20 assert config.telemetry_retry_attempts == 3 assert config.telemetry_retry_backoff_s == 5 def test_display_unit_system_defaults_to_metric(self, clean_env): """DISPLAY_UNIT_SYSTEM defaults to metric.""" config = Config() assert config.display_unit_system == "metric" def test_display_unit_system_accepts_imperial(self, clean_env, monkeypatch): """DISPLAY_UNIT_SYSTEM=imperial is honored.""" monkeypatch.setenv("DISPLAY_UNIT_SYSTEM", "imperial") config = Config() assert config.display_unit_system == "imperial" def test_display_unit_system_invalid_falls_back_to_metric(self, clean_env, monkeypatch): """Invalid DISPLAY_UNIT_SYSTEM falls back to metric.""" monkeypatch.setenv("DISPLAY_UNIT_SYSTEM", "kelvin") config = Config() assert config.display_unit_system == "metric" def test_all_location_settings(self, clean_env, monkeypatch): """All location/report settings are loaded.""" monkeypatch.setenv("REPORT_LOCATION_NAME", "Mountain Peak Observatory") monkeypatch.setenv("REPORT_LOCATION_SHORT", "Mountain Peak") monkeypatch.setenv("REPORT_LAT", "46.8523") monkeypatch.setenv("REPORT_LON", "9.5369") monkeypatch.setenv("REPORT_ELEV", "2500") monkeypatch.setenv("REPORT_ELEV_UNIT", "ft") config = Config() assert config.report_location_name == "Mountain Peak Observatory" assert config.report_location_short == "Mountain Peak" assert config.report_lat == pytest.approx(46.8523) assert config.report_lon == pytest.approx(9.5369) assert config.report_elev == pytest.approx(2500) assert config.report_elev_unit == "ft" def test_all_radio_settings(self, clean_env, monkeypatch): """All radio configuration settings are loaded.""" monkeypatch.setenv("RADIO_FREQUENCY", "915.000 MHz") monkeypatch.setenv("RADIO_BANDWIDTH", "125 kHz") monkeypatch.setenv("RADIO_SPREAD_FACTOR", "SF12") monkeypatch.setenv("RADIO_CODING_RATE", "CR5") config = Config() assert config.radio_frequency == "915.000 MHz" assert config.radio_bandwidth == "125 kHz" assert config.radio_spread_factor == "SF12" assert config.radio_coding_rate == "CR5" def test_companion_settings(self, clean_env, monkeypatch): """Companion display settings are loaded.""" monkeypatch.setenv("COMPANION_DISPLAY_NAME", "Base Station") monkeypatch.setenv("COMPANION_PUBKEY_PREFIX", "!def456") monkeypatch.setenv("COMPANION_HARDWARE", "T-Beam Supreme") config = Config() assert config.companion_display_name == "Base Station" assert config.companion_pubkey_prefix == "!def456" assert config.companion_hardware == "T-Beam Supreme" class TestGetConfigSingleton: """Tests for get_config singleton behavior.""" def test_config_persists_across_calls(self, clean_env, monkeypatch): """Config values persist across multiple get_config calls.""" monkeypatch.setenv("MESH_TRANSPORT", "tcp") config1 = get_config() assert config1.mesh_transport == "tcp" # Change env var - should NOT affect cached config monkeypatch.setenv("MESH_TRANSPORT", "ble") config2 = get_config() assert config2.mesh_transport == "tcp" # Still tcp, cached assert config1 is config2 def test_reset_allows_new_config(self, clean_env, monkeypatch): """Resetting singleton allows new config.""" monkeypatch.setenv("MESH_TRANSPORT", "tcp") config1 = get_config() assert config1.mesh_transport == "tcp" # Reset singleton import meshmon.env meshmon.env._config = None # Change env var monkeypatch.setenv("MESH_TRANSPORT", "ble") config2 = get_config() assert config2.mesh_transport == "ble" assert config1 is not config2