Skip to main content
Glama
pedrof
by pedrof
test_validation.py12.3 kB
""" Unit tests for validation module. Tests all validation functions to ensure proper input checking and error handling. """ import pytest from hue_mcp_server.validation import ( validate_light_id, validate_group_id, validate_scene_id, validate_brightness, validate_color_temperature, validate_xy_coordinates, ) from hue_mcp_server.hue_client import ( BRIGHTNESS_MIN_HUE_API, BRIGHTNESS_MAX_HUE_API, COLOR_TEMP_MIN_MIREDS, COLOR_TEMP_MAX_MIREDS, CIE_XY_MIN, CIE_XY_MAX, ) class TestLightIdValidation: """Tests for light ID validation.""" def test_valid_light_id(self): """Test that valid light IDs pass validation.""" validate_light_id("abc123") validate_light_id("light-id-123") validate_light_id("UUID-format-string") validate_light_id("010d2ba7-9dc7-484f-bf6b-7ab1c8746ca8") # Should not raise def test_empty_light_id_raises_error(self): """Test that empty light IDs are rejected.""" with pytest.raises(ValueError, match="light_id is required"): validate_light_id("") with pytest.raises(ValueError, match="light_id is required"): validate_light_id(" ") def test_none_light_id_raises_error(self): """Test that None light ID is rejected.""" with pytest.raises(ValueError, match="light_id is required"): validate_light_id(None) def test_non_string_light_id_raises_error(self): """Test that non-string light IDs are rejected.""" with pytest.raises(ValueError, match="light_id must be a string"): validate_light_id(123) with pytest.raises(ValueError, match="light_id must be a string"): validate_light_id(["abc"]) with pytest.raises(ValueError, match="light_id must be a string"): validate_light_id({"id": "abc"}) class TestGroupIdValidation: """Tests for group ID validation.""" def test_valid_group_id(self): """Test that valid group IDs pass validation.""" validate_group_id("group-123") validate_group_id("room-456") validate_group_id("zone-789") # Should not raise def test_empty_group_id_raises_error(self): """Test that empty group IDs are rejected.""" with pytest.raises(ValueError, match="group_id is required"): validate_group_id("") with pytest.raises(ValueError, match="group_id is required"): validate_group_id(None) def test_non_string_group_id_raises_error(self): """Test that non-string group IDs are rejected.""" with pytest.raises(ValueError, match="group_id must be a string"): validate_group_id(456) class TestSceneIdValidation: """Tests for scene ID validation.""" def test_valid_scene_id(self): """Test that valid scene IDs pass validation.""" validate_scene_id("scene-123") validate_scene_id("my-scene") # Should not raise def test_empty_scene_id_raises_error(self): """Test that empty scene IDs are rejected.""" with pytest.raises(ValueError, match="scene_id is required"): validate_scene_id("") with pytest.raises(ValueError, match="scene_id is required"): validate_scene_id(None) def test_non_string_scene_id_raises_error(self): """Test that non-string scene IDs are rejected.""" with pytest.raises(ValueError, match="scene_id must be a string"): validate_scene_id(789) class TestBrightnessValidation: """Tests for brightness validation.""" @pytest.mark.parametrize( "brightness", [ BRIGHTNESS_MIN_HUE_API, # 0 1, 50, 127, 200, 253, BRIGHTNESS_MAX_HUE_API, # 254 ], ) def test_valid_brightness_values(self, brightness): """Test that valid brightness values pass validation.""" validate_brightness(brightness) # Should not raise @pytest.mark.parametrize("brightness", [-1, -100, 255, 256, 1000, -999]) def test_brightness_out_of_range_raises_error(self, brightness): """Test that out-of-range brightness values are rejected.""" with pytest.raises(ValueError, match="brightness must be between"): validate_brightness(brightness) @pytest.mark.parametrize("brightness", ["100", "abc", None, [], {}, [127]]) def test_brightness_wrong_type_raises_error(self, brightness): """Test that non-numeric brightness values are rejected.""" with pytest.raises(ValueError, match="brightness must be a number"): validate_brightness(brightness) def test_brightness_float_accepted(self): """Test that float brightness values are accepted.""" validate_brightness(127.5) validate_brightness(0.0) validate_brightness(254.0) validate_brightness(100.5) # Should not raise def test_brightness_error_message_includes_type(self): """Test that error message includes the actual type received.""" with pytest.raises(ValueError, match="got str"): validate_brightness("invalid") with pytest.raises(ValueError, match="got list"): validate_brightness([100]) class TestColorTemperatureValidation: """Tests for color temperature validation.""" @pytest.mark.parametrize( "color_temp", [ COLOR_TEMP_MIN_MIREDS, # 153 154, 200, 300, 400, 499, COLOR_TEMP_MAX_MIREDS, # 500 ], ) def test_valid_color_temp_values(self, color_temp): """Test that valid color temperature values pass validation.""" validate_color_temperature(color_temp) # Should not raise @pytest.mark.parametrize("color_temp", [152, 150, 100, 0, 501, 502, 1000]) def test_color_temp_out_of_range_raises_error(self, color_temp): """Test that out-of-range color temp values are rejected.""" with pytest.raises(ValueError, match="color_temp must be between"): validate_color_temperature(color_temp) def test_color_temp_wrong_type_raises_error(self): """Test that non-numeric color temp values are rejected.""" with pytest.raises(ValueError, match="color_temp must be a number"): validate_color_temperature("300") with pytest.raises(ValueError, match="got NoneType"): validate_color_temperature(None) def test_color_temp_float_accepted(self): """Test that float color temp values are accepted.""" validate_color_temperature(300.0) validate_color_temperature(153.5) # Should not raise def test_color_temp_boundary_values(self): """Test exact boundary values for color temperature.""" # Should pass validate_color_temperature(COLOR_TEMP_MIN_MIREDS) validate_color_temperature(COLOR_TEMP_MAX_MIREDS) # Should fail with pytest.raises(ValueError): validate_color_temperature(COLOR_TEMP_MIN_MIREDS - 1) with pytest.raises(ValueError): validate_color_temperature(COLOR_TEMP_MAX_MIREDS + 1) class TestXYCoordinatesValidation: """Tests for CIE xy coordinates validation.""" @pytest.mark.parametrize( "xy", [ [0.0, 0.0], [1.0, 1.0], [0.5, 0.5], [0.3127, 0.3290], # D65 white point [0.167, 0.04], # Blue [0.7, 0.299], # Red [0.409, 0.518], # Green ], ) def test_valid_xy_coordinates(self, xy): """Test that valid xy coordinates pass validation.""" validate_xy_coordinates(xy) # Should not raise def test_xy_not_list_raises_error(self): """Test that non-list xy values are rejected.""" with pytest.raises(ValueError, match="xy must be a list"): validate_xy_coordinates((0.5, 0.5)) with pytest.raises(ValueError, match="xy must be a list"): validate_xy_coordinates("0.5,0.5") with pytest.raises(ValueError, match="got dict"): validate_xy_coordinates({"x": 0.5, "y": 0.5}) def test_xy_wrong_length_raises_error(self): """Test that xy with wrong number of values is rejected.""" with pytest.raises(ValueError, match="xy must be a list of two values"): validate_xy_coordinates([0.5]) with pytest.raises(ValueError, match="xy must be a list of two values"): validate_xy_coordinates([0.5, 0.5, 0.5]) with pytest.raises(ValueError, match="xy must be a list of two values"): validate_xy_coordinates([]) @pytest.mark.parametrize( "xy", [ [-0.1, 0.5], # x too low [0.5, -0.1], # y too low [1.1, 0.5], # x too high [0.5, 1.1], # y too high [-1.0, 0.5], # x way too low [0.5, 2.0], # y way too high ], ) def test_xy_out_of_range_raises_error(self, xy): """Test that out-of-range xy values are rejected.""" with pytest.raises(ValueError, match="must be between"): validate_xy_coordinates(xy) def test_xy_non_numeric_values_raise_error(self): """Test that non-numeric values in xy are rejected.""" with pytest.raises(ValueError, match="must be a number"): validate_xy_coordinates(["0.5", "0.5"]) with pytest.raises(ValueError, match="must be a number"): validate_xy_coordinates([None, 0.5]) with pytest.raises(ValueError, match="must be a number"): validate_xy_coordinates([0.5, None]) def test_xy_boundary_values(self): """Test exact boundary values for xy coordinates.""" # Should pass validate_xy_coordinates([CIE_XY_MIN, CIE_XY_MIN]) validate_xy_coordinates([CIE_XY_MAX, CIE_XY_MAX]) validate_xy_coordinates([CIE_XY_MIN, CIE_XY_MAX]) validate_xy_coordinates([CIE_XY_MAX, CIE_XY_MIN]) # Should fail with pytest.raises(ValueError): validate_xy_coordinates([CIE_XY_MIN - 0.001, 0.5]) with pytest.raises(ValueError): validate_xy_coordinates([0.5, CIE_XY_MAX + 0.001]) def test_xy_error_message_specifies_coordinate(self): """Test that error messages specify which coordinate (x or y) is invalid.""" with pytest.raises(ValueError, match=r"xy\[0\] \(x\)"): validate_xy_coordinates(["invalid", 0.5]) with pytest.raises(ValueError, match=r"xy\[1\] \(y\)"): validate_xy_coordinates([0.5, "invalid"]) class TestValidationIntegration: """Integration tests for validation functions.""" def test_all_validators_raise_value_error(self): """Test that all validators raise ValueError for invalid input.""" validators = [ (validate_light_id, [None]), (validate_group_id, [None]), (validate_scene_id, [None]), (validate_brightness, [-1]), (validate_color_temperature, [100]), (validate_xy_coordinates, [[2.0, 0.5]]), ] for validator, args in validators: with pytest.raises(ValueError): validator(*args) def test_validators_accept_valid_inputs(self): """Test that all validators accept valid inputs without raising.""" validate_light_id("light-123") validate_group_id("group-456") validate_scene_id("scene-789") validate_brightness(127) validate_color_temperature(300) validate_xy_coordinates([0.5, 0.5]) # Should not raise def test_validators_provide_descriptive_errors(self): """Test that validators provide clear, descriptive error messages.""" try: validate_brightness("not-a-number") except ValueError as e: assert "brightness" in str(e).lower() assert "number" in str(e).lower() assert "str" in str(e).lower() try: validate_xy_coordinates([0.5, 1.5]) except ValueError as e: assert "xy" in str(e).lower() assert "between" in str(e).lower()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pedrof/hue-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server