"""Tests for configuration validation. These tests verify transport mutual exclusivity, BLE PIN requirement, and connection_type derivation. """ import pytest from pydantic import ValidationError from app.config import Settings class TestTransportExclusivity: """Ensure only one transport can be configured at a time.""" def test_no_transport_defaults_to_serial(self): """No transport env vars means serial auto-detect.""" s = Settings(serial_port="", tcp_host="", ble_address="") assert s.connection_type == "serial" def test_serial_only(self): s = Settings(serial_port="/dev/ttyUSB0") assert s.connection_type == "serial" def test_tcp_only(self): s = Settings(tcp_host="192.168.1.1") assert s.connection_type == "tcp" def test_tcp_with_custom_port(self): s = Settings(tcp_host="192.168.1.1", tcp_port=5000) assert s.connection_type == "tcp" assert s.tcp_port == 5000 def test_tcp_default_port(self): s = Settings(tcp_host="192.168.1.1") assert s.tcp_port == 4000 def test_ble_only(self): s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456") assert s.connection_type == "ble" def test_serial_and_tcp_rejected(self): with pytest.raises(ValidationError, match="Only one transport"): Settings(serial_port="/dev/ttyUSB0", tcp_host="192.168.1.1") def test_serial_and_ble_rejected(self): with pytest.raises(ValidationError, match="Only one transport"): Settings( serial_port="/dev/ttyUSB0", ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456", ) def test_tcp_and_ble_rejected(self): with pytest.raises(ValidationError, match="Only one transport"): Settings( tcp_host="192.168.1.1", ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456", ) def test_all_three_rejected(self): with pytest.raises(ValidationError, match="Only one transport"): Settings( serial_port="/dev/ttyUSB0", tcp_host="192.168.1.1", ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456", ) class TestBLEPinRequirement: """BLE address requires a PIN.""" def test_ble_address_without_pin_rejected(self): with pytest.raises(ValidationError, match="MESHCORE_BLE_PIN is required"): Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="") def test_ble_address_with_pin_accepted(self): s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456") assert s.ble_address == "AA:BB:CC:DD:EE:FF" assert s.ble_pin == "123456" class TestBasicAuthConfiguration: """Ensure basic auth credentials are configured as a pair.""" def test_basic_auth_disabled_by_default(self): s = Settings(serial_port="", tcp_host="", ble_address="") assert s.basic_auth_enabled is False def test_basic_auth_enabled_when_both_credentials_are_set(self): s = Settings( serial_port="", tcp_host="", ble_address="", basic_auth_username="mesh", basic_auth_password="secret", ) assert s.basic_auth_enabled is True def test_basic_auth_requires_password_with_username(self): with pytest.raises(ValidationError, match="MESHCORE_BASIC_AUTH_USERNAME"): Settings( serial_port="", tcp_host="", ble_address="", basic_auth_username="mesh", basic_auth_password="", ) def test_basic_auth_requires_username_with_password(self): with pytest.raises(ValidationError, match="MESHCORE_BASIC_AUTH_USERNAME"): Settings( serial_port="", tcp_host="", ble_address="", basic_auth_username="", basic_auth_password="secret", ) class TestExperimentalAliases: """Ensure exact-name experimental env vars still map into settings.""" def test_clowntown_wraparound_alias_reads_exact_env_var(self, monkeypatch): monkeypatch.setenv("__CLOWNTOWN_DO_CLOCK_WRAPAROUND", "true") s = Settings(serial_port="", tcp_host="", ble_address="") assert s.clowntown_do_clock_wraparound is True