"""
Unit tests for HueClient helper functions and utilities.
These tests focus on the pure functions and logic that don't require
a connection to the actual Hue Bridge.
"""
import pytest
from hue_mcp_server.hue_client import (
HueClient,
_convert_brightness_to_percentage,
_is_device_resource,
BRIGHTNESS_MIN_HUE_API,
BRIGHTNESS_MAX_HUE_API,
BRIGHTNESS_MIN_PERCENT,
BRIGHTNESS_MAX_PERCENT,
COLOR_TEMP_MIN_MIREDS,
COLOR_TEMP_MAX_MIREDS,
CIE_XY_MIN,
CIE_XY_MAX,
DEVICE_RESOURCE_TYPE,
)
class TestConstants:
"""Tests for module-level constants."""
def test_brightness_constants_defined(self):
"""Test that brightness constants are defined correctly."""
assert BRIGHTNESS_MIN_HUE_API == 0
assert BRIGHTNESS_MAX_HUE_API == 254
assert BRIGHTNESS_MIN_PERCENT == 0.0
assert BRIGHTNESS_MAX_PERCENT == 100.0
def test_color_temp_constants_defined(self):
"""Test that color temperature constants are defined correctly."""
assert COLOR_TEMP_MIN_MIREDS == 153
assert COLOR_TEMP_MAX_MIREDS == 500
def test_cie_xy_constants_defined(self):
"""Test that CIE xy color space constants are defined correctly."""
assert CIE_XY_MIN == 0.0
assert CIE_XY_MAX == 1.0
def test_device_resource_type_constant(self):
"""Test that device resource type constant is defined."""
assert DEVICE_RESOURCE_TYPE == "device"
class TestBrightnessConversion:
"""Tests for brightness conversion helper function."""
def test_convert_min_brightness(self):
"""Test conversion of minimum brightness (0 -> 0%)."""
result = _convert_brightness_to_percentage(BRIGHTNESS_MIN_HUE_API)
assert result == BRIGHTNESS_MIN_PERCENT
assert result == 0.0
def test_convert_max_brightness(self):
"""Test conversion of maximum brightness (254 -> 100%)."""
result = _convert_brightness_to_percentage(BRIGHTNESS_MAX_HUE_API)
assert result == BRIGHTNESS_MAX_PERCENT
assert result == 100.0
def test_convert_mid_brightness(self):
"""Test conversion of mid-range brightness (~127 -> ~50%)."""
result = _convert_brightness_to_percentage(127)
# 127/254 * 100 = ~50%
assert 49.0 <= result <= 51.0
def test_convert_quarter_brightness(self):
"""Test conversion of quarter brightness (~64 -> ~25%)."""
result = _convert_brightness_to_percentage(64)
# 64/254 * 100 = ~25.2%
assert 24.0 <= result <= 26.0
def test_convert_three_quarter_brightness(self):
"""Test conversion of three-quarter brightness (~190 -> ~75%)."""
result = _convert_brightness_to_percentage(190)
# 190/254 * 100 = ~74.8%
assert 73.0 <= result <= 76.0
def test_conversion_is_linear(self):
"""Test that conversion is linear across the range."""
# Test several points to ensure linearity
test_points = [0, 50, 100, 150, 200, 254]
results = [_convert_brightness_to_percentage(b) for b in test_points]
# Verify results are in ascending order
assert results == sorted(results)
# Verify correct calculation for each point
for brightness, result in zip(test_points, results):
expected = (brightness / BRIGHTNESS_MAX_HUE_API) * BRIGHTNESS_MAX_PERCENT
assert abs(result - expected) < 0.01 # Allow tiny floating point error
def test_conversion_returns_float(self):
"""Test that conversion always returns a float."""
result = _convert_brightness_to_percentage(100)
assert isinstance(result, float)
@pytest.mark.parametrize(
"hue_value,expected_percent",
[
(0, 0.0),
(25, 9.84), # Approximately
(51, 20.08),
(127, 50.0),
(203, 79.92),
(254, 100.0),
],
)
def test_conversion_specific_values(self, hue_value, expected_percent):
"""Test conversion for specific known values."""
result = _convert_brightness_to_percentage(hue_value)
assert abs(result - expected_percent) < 0.5 # Within 0.5%
class TestDeviceResourceCheck:
"""Tests for device resource type checker function."""
def test_is_device_resource_returns_true_for_device(self):
"""Test that 'device' type returns True."""
assert _is_device_resource("device") is True
def test_is_device_resource_returns_false_for_light(self):
"""Test that 'light' type returns False."""
assert _is_device_resource("light") is False
def test_is_device_resource_returns_false_for_group(self):
"""Test that 'group' type returns False."""
assert _is_device_resource("group") is False
def test_is_device_resource_returns_false_for_scene(self):
"""Test that 'scene' type returns False."""
assert _is_device_resource("scene") is False
def test_is_device_resource_returns_false_for_room(self):
"""Test that 'room' type returns False."""
assert _is_device_resource("room") is False
def test_is_device_resource_returns_false_for_zone(self):
"""Test that 'zone' type returns False."""
assert _is_device_resource("zone") is False
def test_is_device_resource_case_sensitive(self):
"""Test that resource type check is case-sensitive."""
assert _is_device_resource("Device") is False
assert _is_device_resource("DEVICE") is False
assert _is_device_resource("device") is True
def test_is_device_resource_with_empty_string(self):
"""Test that empty string returns False."""
assert _is_device_resource("") is False
@pytest.mark.parametrize(
"resource_type,expected",
[
("device", True),
("light", False),
("group", False),
("scene", False),
("room", False),
("zone", False),
("bridge", False),
("entertainment", False),
("behavior_instance", False),
],
)
def test_is_device_resource_various_types(self, resource_type, expected):
"""Test device resource check for various Hue API resource types."""
assert _is_device_resource(resource_type) == expected
class TestHueClientInitialization:
"""Tests for HueClient initialization (no bridge connection needed)."""
def test_client_initialization_with_valid_parameters(self):
"""Test that client initializes correctly with valid parameters."""
client = HueClient("192.168.1.100", "test-api-key-123")
assert client.bridge_ip == "192.168.1.100"
assert client.api_key == "test-api-key-123"
assert client.bridge is None
assert client.is_connected() is False
def test_client_stores_bridge_ip_correctly(self):
"""Test that bridge IP is stored as provided."""
test_ips = ["192.168.1.1", "10.0.0.50", "172.16.0.100"]
for ip in test_ips:
client = HueClient(ip, "key")
assert client.bridge_ip == ip
def test_client_stores_api_key_correctly(self):
"""Test that API key is stored as provided."""
test_keys = [
"short-key",
"a" * 40, # 40 character key (typical Hue key length)
"complex-key-with-numbers-123",
]
for key in test_keys:
client = HueClient("192.168.1.1", key)
assert client.api_key == key
def test_client_not_connected_initially(self):
"""Test that client is not connected after initialization."""
client = HueClient("192.168.1.100", "test-api-key")
assert client.is_connected() is False
def test_client_bridge_is_none_initially(self):
"""Test that bridge reference is None after initialization."""
client = HueClient("192.168.1.100", "test-api-key")
assert client.bridge is None
def test_multiple_clients_are_independent(self):
"""Test that multiple client instances are independent."""
client1 = HueClient("192.168.1.1", "key1")
client2 = HueClient("192.168.1.2", "key2")
assert client1.bridge_ip != client2.bridge_ip
assert client1.api_key != client2.api_key
assert client1 is not client2
class TestHueClientConnectionState:
"""Tests for HueClient connection state management (no actual connection)."""
def test_is_connected_returns_false_initially(self):
"""Test that is_connected returns False before connection."""
client = HueClient("192.168.1.100", "test-api-key")
assert client.is_connected() is False
def test_is_connected_returns_boolean(self):
"""Test that is_connected always returns a boolean."""
client = HueClient("192.168.1.100", "test-api-key")
result = client.is_connected()
assert isinstance(result, bool)
class TestHueClientEdgeCases:
"""Tests for edge cases in HueClient."""
def test_client_with_localhost_ip(self):
"""Test client initialization with localhost IP."""
client = HueClient("127.0.0.1", "test-key")
assert client.bridge_ip == "127.0.0.1"
def test_client_with_ipv6_address(self):
"""Test client initialization with IPv6 address."""
client = HueClient("fe80::1", "test-key")
assert client.bridge_ip == "fe80::1"
def test_client_with_hostname(self):
"""Test client initialization with hostname instead of IP."""
client = HueClient("philips-hue.local", "test-key")
assert client.bridge_ip == "philips-hue.local"
def test_client_with_very_long_api_key(self):
"""Test client with unusually long API key."""
long_key = "a" * 1000
client = HueClient("192.168.1.100", long_key)
assert client.api_key == long_key
assert len(client.api_key) == 1000
def test_client_with_empty_api_key(self):
"""Test client initialization with empty API key."""
# Client should initialize even with empty key (will fail on connect)
client = HueClient("192.168.1.100", "")
assert client.api_key == ""
def test_client_with_special_characters_in_key(self):
"""Test client with special characters in API key."""
special_key = "key-with-!@#$%^&*()_+-=[]{}|;:',.<>?"
client = HueClient("192.168.1.100", special_key)
assert client.api_key == special_key