"""
Unit tests for SmartThingsMCP device operations.
Tests the devices module and device-related MCP tools.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from SmartThingsMCP.modules.server.common import (
make_request,
build_url,
build_device_url,
filter_none_params,
BASE_URL
)
class TestCommonUtilities:
"""Test common utility functions used by device tools."""
def test_build_url_devices(self):
"""Test building URL for devices endpoint."""
url = build_url("devices")
assert url == f"{BASE_URL}/devices"
def test_build_url_locations(self):
"""Test building URL for locations endpoint."""
url = build_url("locations")
assert url == f"{BASE_URL}/locations"
def test_build_device_url(self):
"""Test building URL for specific device."""
device_id = "test-device-123"
url = build_device_url(device_id)
assert url == f"{BASE_URL}/devices/{device_id}"
def test_build_device_url_with_subpath(self):
"""Test building URL for device subpath."""
device_id = "test-device-123"
subpath = "status"
url = build_device_url(device_id, subpath)
assert url == f"{BASE_URL}/devices/{device_id}/{subpath}"
def test_filter_none_params_removes_none(self):
"""Test that filter_none_params removes None values."""
params = {
"device_id": "123",
"location_id": None,
"capability": "switch",
"room_id": None
}
filtered = filter_none_params(**params)
assert filtered == {
"device_id": "123",
"capability": "switch"
}
assert "location_id" not in filtered
assert "room_id" not in filtered
def test_filter_none_params_empty(self):
"""Test filter_none_params with all None values."""
params = {
"device_id": None,
"location_id": None
}
filtered = filter_none_params(**params)
assert filtered == {}
def test_filter_none_params_no_none(self):
"""Test filter_none_params with no None values."""
params = {
"device_id": "123",
"location_id": "loc-456"
}
filtered = filter_none_params(**params)
assert filtered == params
class TestMakeRequest:
"""Test the make_request function.
Note: These tests are difficult to mock due to caching in the actual implementation.
Focus is on the decorator patterns that wrap make_request in the actual modules.
"""
def test_make_request_signature(self):
"""Test that make_request has correct signature."""
# Verify the function exists and has the right parameters
import inspect
sig = inspect.signature(make_request)
params = list(sig.parameters.keys())
assert "auth" in params
assert "method" in params
assert "url" in params
assert "params" in params
assert "data" in params
assert "headers" in params
def test_build_url_construction(self):
"""Test URL building for requests."""
base_url = "https://api.smartthings.com/v1"
# Test basic endpoint
devices_url = f"{base_url}/devices"
assert "devices" in devices_url
# Test with parameters
location_id = "loc-123"
rooms_url = f"{base_url}/locations/{location_id}/rooms"
assert location_id in rooms_url
def test_header_building(self):
"""Test that headers would be built correctly."""
token = "test-auth-token-12345"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
assert "Authorization" in headers
assert headers["Authorization"] == f"Bearer {token}"
assert "Content-Type" in headers
class TestDeviceTools:
"""Test device-related MCP tools."""
@patch('SmartThingsMCP.modules.server.common.make_request')
def test_list_devices_basic(self, mock_request):
"""Test listing all devices."""
mock_request.return_value = {
"items": [
{"id": "device-1", "name": "Light 1", "status": "online"},
{"id": "device-2", "name": "Switch 1", "status": "offline"}
]
}
# Import here to ensure patches are applied
from SmartThingsMCP.modules.server.devices import register_tools
mock_server = Mock()
mock_server.tool = Mock(return_value=lambda f: f)
register_tools(mock_server)
# Verify register_tools was called
assert mock_server.tool.called or mock_server.add_tool.called
@patch('SmartThingsMCP.modules.server.common.make_request')
def test_list_devices_with_capability_filter(self, mock_request):
"""Test listing devices filtered by capability."""
mock_request.return_value = {
"items": [
{"id": "device-1", "name": "Light 1", "capabilities": ["switch"]}
]
}
# This would be called as: list_devices(auth="token", capability="switch")
# The actual implementation uses the decorator pattern
assert mock_request.return_value["items"][0]["capabilities"] == ["switch"]
@patch('SmartThingsMCP.modules.server.common.make_request')
def test_list_devices_with_location_filter(self, mock_request):
"""Test listing devices filtered by location."""
mock_request.return_value = {
"items": [
{"id": "device-1", "name": "Light 1", "locationId": "loc-123"}
]
}
assert mock_request.return_value["items"][0]["locationId"] == "loc-123"
@patch('SmartThingsMCP.modules.server.common.make_request')
def test_get_device_status(self, mock_request):
"""Test getting device status."""
mock_request.return_value = {
"id": "device-1",
"name": "Light 1",
"components": [
{
"id": "main",
"capabilities": [
{
"id": "switch",
"values": [{"key": "switch", "value": "on"}]
}
]
}
]
}
assert mock_request.return_value["id"] == "device-1"
assert len(mock_request.return_value["components"]) > 0
@patch('SmartThingsMCP.modules.server.common.make_request')
def test_execute_device_command(self, mock_request):
"""Test executing a command on a device."""
mock_request.return_value = {
"id": "cmd-123",
"status": "ACCEPTED"
}
assert mock_request.return_value["status"] == "ACCEPTED"
class TestDeviceFiltering:
"""Test device filtering and search functionality."""
def test_filter_by_capability(self):
"""Test filtering devices by capability."""
devices = [
{"id": "1", "name": "Light 1", "capabilities": ["switch", "brightness"]},
{"id": "2", "name": "Switch 2", "capabilities": ["switch"]},
{"id": "3", "name": "Lock 3", "capabilities": ["lock"]}
]
filtered = [d for d in devices if "switch" in d.get("capabilities", [])]
assert len(filtered) == 2
assert all("switch" in d["capabilities"] for d in filtered)
def test_filter_by_status(self):
"""Test filtering devices by status."""
devices = [
{"id": "1", "name": "Light 1", "status": "online"},
{"id": "2", "name": "Switch 2", "status": "offline"},
{"id": "3", "name": "Lock 3", "status": "online"}
]
online_devices = [d for d in devices if d.get("status") == "online"]
assert len(online_devices) == 2
assert all(d["status"] == "online" for d in online_devices)
def test_filter_by_location(self):
"""Test filtering devices by location."""
devices = [
{"id": "1", "name": "Light 1", "locationId": "loc-1"},
{"id": "2", "name": "Switch 2", "locationId": "loc-2"},
{"id": "3", "name": "Lock 3", "locationId": "loc-1"}
]
location_devices = [d for d in devices if d.get("locationId") == "loc-1"]
assert len(location_devices) == 2
assert all(d["locationId"] == "loc-1" for d in location_devices)
class TestDeviceCommands:
"""Test device command execution."""
def test_turn_on_command(self):
"""Test turn on command payload."""
command = {
"commands": [
{
"component": "main",
"capability": "switch",
"command": "on"
}
]
}
assert command["commands"][0]["command"] == "on"
assert command["commands"][0]["capability"] == "switch"
def test_turn_off_command(self):
"""Test turn off command payload."""
command = {
"commands": [
{
"component": "main",
"capability": "switch",
"command": "off"
}
]
}
assert command["commands"][0]["command"] == "off"
def test_set_brightness_command(self):
"""Test brightness setting command."""
command = {
"commands": [
{
"component": "main",
"capability": "switchLevel",
"command": "setLevel",
"arguments": [75]
}
]
}
assert command["commands"][0]["command"] == "setLevel"
assert command["commands"][0]["arguments"][0] == 75
def test_set_color_command(self):
"""Test color setting command."""
command = {
"commands": [
{
"component": "main",
"capability": "colorControl",
"command": "setColor",
"arguments": [{"hue": 100, "saturation": 100}]
}
]
}
assert command["commands"][0]["command"] == "setColor"
assert command["commands"][0]["arguments"][0]["hue"] == 100