mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 10:44:04 +00:00
Improve CLI Discovery output (#583)
- Show discovery results for unsupported devices and devices that fail to authenticate. - Rename `--show-unsupported` to `--verbose`. - Remove separate `--timeout` parameter from cli discovery so it's not confused with `--timeout` now added to cli command. - Add tests.
This commit is contained in:
@@ -129,6 +129,37 @@ ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
||||
IP_MODEL_CACHE: Dict[str, str] = {}
|
||||
|
||||
|
||||
def _make_unsupported(device_family, encrypt_type):
|
||||
return {
|
||||
"result": {
|
||||
"device_id": "xx",
|
||||
"owner": "xx",
|
||||
"device_type": device_family,
|
||||
"device_model": "P110(EU)",
|
||||
"ip": "127.0.0.1",
|
||||
"mac": "48-22xxx",
|
||||
"is_support_iot_cloud": True,
|
||||
"obd_src": "tplink",
|
||||
"factory_default": False,
|
||||
"mgt_encrypt_schm": {
|
||||
"is_support_https": False,
|
||||
"encrypt_type": encrypt_type,
|
||||
"http_port": 80,
|
||||
"lv": 2,
|
||||
},
|
||||
},
|
||||
"error_code": 0,
|
||||
}
|
||||
|
||||
|
||||
UNSUPPORTED_DEVICES = {
|
||||
"unknown_device_family": _make_unsupported("SMART.TAPOXMASTREE", "AES"),
|
||||
"wrong_encryption_iot": _make_unsupported("IOT.SMARTPLUGSWITCH", "AES"),
|
||||
"wrong_encryption_smart": _make_unsupported("SMART.TAPOBULB", "IOT"),
|
||||
"unknown_encryption": _make_unsupported("IOT.SMARTPLUGSWITCH", "FOO"),
|
||||
}
|
||||
|
||||
|
||||
def idgenerator(paramtuple):
|
||||
try:
|
||||
return basename(paramtuple[0]) + (
|
||||
@@ -242,7 +273,7 @@ def filter_fixtures(desc, root_filter):
|
||||
def parametrize_discovery(desc, root_key):
|
||||
filtered_fixtures = filter_fixtures(desc, root_key)
|
||||
return pytest.mark.parametrize(
|
||||
"discovery_data",
|
||||
"all_fixture_data",
|
||||
filtered_fixtures.values(),
|
||||
indirect=True,
|
||||
ids=filtered_fixtures.keys(),
|
||||
@@ -360,7 +391,7 @@ async def get_device_for_file(file, protocol):
|
||||
return d
|
||||
|
||||
|
||||
@pytest.fixture(params=SUPPORTED_DEVICES)
|
||||
@pytest.fixture(params=SUPPORTED_DEVICES, ids=idgenerator)
|
||||
async def dev(request):
|
||||
"""Device fixture.
|
||||
|
||||
@@ -386,23 +417,27 @@ async def dev(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def discovery_mock(discovery_data, mocker):
|
||||
def discovery_mock(all_fixture_data, mocker):
|
||||
@dataclass
|
||||
class _DiscoveryMock:
|
||||
ip: str
|
||||
default_port: int
|
||||
discovery_data: dict
|
||||
query_data: dict
|
||||
port_override: Optional[int] = None
|
||||
|
||||
if "result" in discovery_data:
|
||||
if "discovery_result" in all_fixture_data:
|
||||
discovery_data = {"result": all_fixture_data["discovery_result"]}
|
||||
datagram = (
|
||||
b"\x02\x00\x00\x01\x01[\x00\x00\x00\x00\x00\x00W\xcev\xf8"
|
||||
+ json_dumps(discovery_data).encode()
|
||||
)
|
||||
dm = _DiscoveryMock("127.0.0.123", 20002, discovery_data)
|
||||
dm = _DiscoveryMock("127.0.0.123", 20002, discovery_data, all_fixture_data)
|
||||
else:
|
||||
sys_info = all_fixture_data["system"]["get_sysinfo"]
|
||||
discovery_data = {"system": {"get_sysinfo": sys_info}}
|
||||
datagram = TPLinkSmartHomeProtocol.encrypt(json_dumps(discovery_data))[4:]
|
||||
dm = _DiscoveryMock("127.0.0.123", 9999, discovery_data)
|
||||
dm = _DiscoveryMock("127.0.0.123", 9999, discovery_data, all_fixture_data)
|
||||
|
||||
def mock_discover(self):
|
||||
port = (
|
||||
@@ -420,17 +455,29 @@ def discovery_mock(discovery_data, mocker):
|
||||
"socket.getaddrinfo",
|
||||
side_effect=lambda *_, **__: [(None, None, None, None, (dm.ip, 0))],
|
||||
)
|
||||
|
||||
if "component_nego" in dm.query_data:
|
||||
proto = FakeSmartProtocol(dm.query_data)
|
||||
else:
|
||||
proto = FakeTransportProtocol(dm.query_data)
|
||||
|
||||
async def _query(request, retry_count: int = 3):
|
||||
return await proto.query(request)
|
||||
|
||||
mocker.patch("kasa.IotProtocol.query", side_effect=_query)
|
||||
mocker.patch("kasa.SmartProtocol.query", side_effect=_query)
|
||||
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", side_effect=_query)
|
||||
|
||||
yield dm
|
||||
|
||||
|
||||
@pytest.fixture(params=FIXTURE_DATA.values(), ids=FIXTURE_DATA.keys(), scope="session")
|
||||
def discovery_data(request):
|
||||
@pytest.fixture
|
||||
def discovery_data(all_fixture_data):
|
||||
"""Return raw discovery file contents as JSON. Used for discovery tests."""
|
||||
fixture_data = request.param
|
||||
if "discovery_result" in fixture_data:
|
||||
return {"result": fixture_data["discovery_result"]}
|
||||
if "discovery_result" in all_fixture_data:
|
||||
return {"result": all_fixture_data["discovery_result"]}
|
||||
else:
|
||||
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
|
||||
return {"system": {"get_sysinfo": all_fixture_data["system"]["get_sysinfo"]}}
|
||||
|
||||
|
||||
@pytest.fixture(params=FIXTURE_DATA.values(), ids=FIXTURE_DATA.keys(), scope="session")
|
||||
@@ -440,6 +487,25 @@ def all_fixture_data(request):
|
||||
return fixture_data
|
||||
|
||||
|
||||
@pytest.fixture(params=UNSUPPORTED_DEVICES.values(), ids=UNSUPPORTED_DEVICES.keys())
|
||||
def unsupported_device_info(request, mocker):
|
||||
"""Return unsupported devices for cli and discovery tests."""
|
||||
discovery_data = request.param
|
||||
host = "127.0.0.1"
|
||||
|
||||
def mock_discover(self):
|
||||
if discovery_data:
|
||||
data = (
|
||||
b"\x02\x00\x00\x01\x01[\x00\x00\x00\x00\x00\x00W\xcev\xf8"
|
||||
+ json_dumps(discovery_data).encode()
|
||||
)
|
||||
self.datagram_received(data, (host, 20002))
|
||||
|
||||
mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
|
||||
|
||||
yield discovery_data
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--ip", action="store", default=None, help="run against device on given ip"
|
||||
|
@@ -291,6 +291,13 @@ class FakeSmartProtocol(SmartProtocol):
|
||||
def __init__(self, info):
|
||||
super().__init__("127.0.0.123", transport=FakeSmartTransport(info))
|
||||
|
||||
async def query(self, request, retry_count: int = 3):
|
||||
"""Implement query here so can still patch SmartProtocol.query."""
|
||||
resp_dict = await self._query(request, retry_count)
|
||||
if "result" in resp_dict:
|
||||
return resp_dict["result"]
|
||||
return {}
|
||||
|
||||
|
||||
class FakeSmartTransport(BaseTransport):
|
||||
def __init__(self, info):
|
||||
|
@@ -4,14 +4,12 @@ import asyncclick as click
|
||||
import pytest
|
||||
from asyncclick.testing import CliRunner
|
||||
|
||||
from kasa import SmartDevice, TPLinkSmartHomeProtocol
|
||||
from kasa import AuthenticationException, SmartDevice, UnsupportedDeviceException
|
||||
from kasa.cli import alias, brightness, cli, emeter, raw_command, state, sysinfo, toggle
|
||||
from kasa.device_factory import DEVICE_TYPE_TO_CLASS
|
||||
from kasa.discover import Discover
|
||||
from kasa.smartprotocol import SmartProtocol
|
||||
|
||||
from .conftest import device_iot, handle_turn_on, new_discovery, turn_on
|
||||
from .newfakes import FakeSmartProtocol, FakeTransportProtocol
|
||||
|
||||
|
||||
@device_iot
|
||||
@@ -22,7 +20,6 @@ async def test_sysinfo(dev):
|
||||
assert dev.alias in res.output
|
||||
|
||||
|
||||
@device_iot
|
||||
@turn_on
|
||||
async def test_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
@@ -36,7 +33,6 @@ async def test_state(dev, turn_on):
|
||||
assert "Device state: False" in res.output
|
||||
|
||||
|
||||
@device_iot
|
||||
@turn_on
|
||||
async def test_toggle(dev, turn_on, mocker):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
@@ -226,3 +222,123 @@ async def test_duplicate_target_device():
|
||||
)
|
||||
assert res.exit_code == 2
|
||||
assert "Error: Use either --alias or --host, not both." in res.output
|
||||
|
||||
|
||||
async def test_discover(discovery_mock, mocker):
|
||||
"""Test discovery output."""
|
||||
runner = CliRunner()
|
||||
res = await runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--discovery-timeout",
|
||||
0,
|
||||
"--username",
|
||||
"foo",
|
||||
"--password",
|
||||
"bar",
|
||||
"discover",
|
||||
"--verbose",
|
||||
],
|
||||
)
|
||||
assert res.exit_code == 0
|
||||
|
||||
|
||||
async def test_discover_unsupported(unsupported_device_info):
|
||||
"""Test discovery output."""
|
||||
runner = CliRunner()
|
||||
res = await runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--discovery-timeout",
|
||||
0,
|
||||
"--username",
|
||||
"foo",
|
||||
"--password",
|
||||
"bar",
|
||||
"discover",
|
||||
"--verbose",
|
||||
],
|
||||
)
|
||||
assert res.exit_code == 0
|
||||
assert "== Unsupported device ==" in res.output
|
||||
assert "== Discovery Result ==" in res.output
|
||||
|
||||
|
||||
async def test_host_unsupported(unsupported_device_info):
|
||||
"""Test discovery output."""
|
||||
runner = CliRunner()
|
||||
host = "127.0.0.1"
|
||||
|
||||
res = await runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--host",
|
||||
host,
|
||||
"--username",
|
||||
"foo",
|
||||
"--password",
|
||||
"bar",
|
||||
],
|
||||
)
|
||||
|
||||
assert res.exit_code != 0
|
||||
assert isinstance(res.exception, UnsupportedDeviceException)
|
||||
|
||||
|
||||
@new_discovery
|
||||
async def test_discover_auth_failed(discovery_mock, mocker):
|
||||
"""Test discovery output."""
|
||||
runner = CliRunner()
|
||||
host = "127.0.0.1"
|
||||
discovery_mock.ip = host
|
||||
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
||||
mocker.patch.object(
|
||||
device_class,
|
||||
"update",
|
||||
side_effect=AuthenticationException("Failed to authenticate"),
|
||||
)
|
||||
res = await runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--discovery-timeout",
|
||||
0,
|
||||
"--username",
|
||||
"foo",
|
||||
"--password",
|
||||
"bar",
|
||||
"discover",
|
||||
"--verbose",
|
||||
],
|
||||
)
|
||||
|
||||
assert res.exit_code == 0
|
||||
assert "== Authentication failed for device ==" in res.output
|
||||
assert "== Discovery Result ==" in res.output
|
||||
|
||||
|
||||
@new_discovery
|
||||
async def test_host_auth_failed(discovery_mock, mocker):
|
||||
"""Test discovery output."""
|
||||
runner = CliRunner()
|
||||
host = "127.0.0.1"
|
||||
discovery_mock.ip = host
|
||||
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
||||
mocker.patch.object(
|
||||
device_class,
|
||||
"update",
|
||||
side_effect=AuthenticationException("Failed to authenticate"),
|
||||
)
|
||||
res = await runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--host",
|
||||
host,
|
||||
"--username",
|
||||
"foo",
|
||||
"--password",
|
||||
"bar",
|
||||
],
|
||||
)
|
||||
|
||||
assert res.exit_code != 0
|
||||
assert isinstance(res.exception, AuthenticationException)
|
||||
|
@@ -202,7 +202,7 @@ async def test_discover_datagram_received(mocker, discovery_data):
|
||||
# Check that device in discovered_devices is initialized correctly
|
||||
assert len(proto.discovered_devices) == 1
|
||||
# Check that unsupported device is 1
|
||||
assert len(proto.unsupported_devices) == 1
|
||||
assert len(proto.unsupported_device_exceptions) == 1
|
||||
dev = proto.discovered_devices[addr]
|
||||
assert issubclass(dev.__class__, SmartDevice)
|
||||
assert dev.host == addr
|
||||
|
Reference in New Issue
Block a user