mirror of
https://github.com/python-kasa/python-kasa.git
synced 2026-07-11 01:42:05 +00:00
Some checks failed
CI / Perform Lint Checks (3.14) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.14 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.14 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CI / Python 3.14 on windows-latest (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
type annotations and parameter type annotations to all test functions and helper functions across the 5 transport test files. This enables mypy to check function bodies, catching type errors that were previously hidden.
148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
import base64
|
|
from unittest.mock import ANY
|
|
|
|
import aiohttp
|
|
import pytest
|
|
from pytest_mock import MockerFixture
|
|
from yarl import URL
|
|
|
|
from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_credentials
|
|
from kasa.deviceconfig import DeviceConfig
|
|
from kasa.exceptions import KasaException
|
|
from kasa.httpclient import HttpClient
|
|
from kasa.json import dumps as json_dumps
|
|
from kasa.transports.linkietransport import LinkieTransportV2
|
|
|
|
KASACAM_REQUEST_PLAINTEXT = '{"smartlife.cam.ipcamera.dateTime":{"get_status":{}}}'
|
|
KASACAM_RESPONSE_ENCRYPTED = "0PKG74LnnfKc+dvhw5bCgaycqZOjk7Gdv96syaiKsJLTvtupwKPC7aPGse632KrB48/tiPiX9JzDsNW2lK6fqZCgmKuZoZGh3A=="
|
|
KASACAM_RESPONSE_ERROR = '{"smartlife.cam.ipcamera.cloud": {"get_inf": {"err_code": -10008, "err_msg": "Unsupported API call."}}}'
|
|
KASA_DEFAULT_CREDENTIALS_HASH = "YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="
|
|
|
|
|
|
async def test_working(mocker: MockerFixture) -> None:
|
|
"""No errors with an expected request/response."""
|
|
host = "127.0.0.1"
|
|
mock_linkie_device = MockLinkieDevice(host)
|
|
mocker.patch.object(
|
|
aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
|
|
)
|
|
transport_no_creds = LinkieTransportV2(config=DeviceConfig(host))
|
|
|
|
response = await transport_no_creds.send(KASACAM_REQUEST_PLAINTEXT)
|
|
assert response == {
|
|
"timezone": "UTC-05:00",
|
|
"area": "America/New_York",
|
|
"epoch_sec": 1690832800,
|
|
}
|
|
|
|
|
|
async def test_credentials_hash(mocker: MockerFixture) -> None:
|
|
"""Ensure the default credentials are always passed as Basic Auth."""
|
|
# Test without credentials input
|
|
|
|
host = "127.0.0.1"
|
|
mock_linkie_device = MockLinkieDevice(host)
|
|
mock_post = mocker.patch.object(
|
|
aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
|
|
)
|
|
transport_no_creds = LinkieTransportV2(config=DeviceConfig(host))
|
|
await transport_no_creds.send(KASACAM_REQUEST_PLAINTEXT)
|
|
mock_post.assert_called_once_with(
|
|
URL(f"https://{host}:10443/data/LINKIE2.json"),
|
|
params=None,
|
|
data=ANY,
|
|
json=None,
|
|
timeout=ANY,
|
|
cookies=None,
|
|
headers={
|
|
"Authorization": "Basic " + _generate_kascam_basic_auth(),
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
ssl=ANY,
|
|
)
|
|
|
|
assert transport_no_creds.credentials_hash == KASA_DEFAULT_CREDENTIALS_HASH
|
|
# Test with credentials input
|
|
|
|
transport_with_creds = LinkieTransportV2(
|
|
config=DeviceConfig(host, credentials=Credentials("Admin", "password"))
|
|
)
|
|
mock_post.reset_mock()
|
|
|
|
await transport_with_creds.send(KASACAM_REQUEST_PLAINTEXT)
|
|
mock_post.assert_called_once_with(
|
|
URL(f"https://{host}:10443/data/LINKIE2.json"),
|
|
params=None,
|
|
data=ANY,
|
|
json=None,
|
|
timeout=ANY,
|
|
cookies=None,
|
|
headers={
|
|
"Authorization": "Basic " + _generate_kascam_basic_auth(),
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
ssl=ANY,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("return_status", "return_data", "expected"),
|
|
[
|
|
(500, KASACAM_RESPONSE_ENCRYPTED, "500"),
|
|
(200, "AAAAAAAAAAAAAAAAAAAAAAAA", "Unable to read response"),
|
|
(200, KASACAM_RESPONSE_ERROR, "Unsupported API call"),
|
|
],
|
|
)
|
|
async def test_exceptions(
|
|
mocker: MockerFixture, return_status: int, return_data: str, expected: str
|
|
) -> None:
|
|
"""Test a variety of possible responses from the device."""
|
|
host = "127.0.0.1"
|
|
transport = LinkieTransportV2(config=DeviceConfig(host))
|
|
mock_linkie_device = MockLinkieDevice(
|
|
host, status_code=return_status, response=return_data
|
|
)
|
|
mocker.patch.object(
|
|
aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
|
|
)
|
|
|
|
with pytest.raises(KasaException, match=expected):
|
|
await transport.send(KASACAM_REQUEST_PLAINTEXT)
|
|
|
|
|
|
def _generate_kascam_basic_auth() -> str:
|
|
creds = get_default_credentials(DEFAULT_CREDENTIALS["KASACAMERA"])
|
|
creds_combined = f"{creds.username}:{creds.password}"
|
|
return base64.b64encode(creds_combined.encode()).decode()
|
|
|
|
|
|
class MockLinkieDevice:
|
|
"""Based on MockSslDevice."""
|
|
|
|
class _mock_response:
|
|
def __init__(self, status, request: dict):
|
|
self.status = status
|
|
self._json = request
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_t, exc_v, exc_tb):
|
|
pass
|
|
|
|
async def read(self):
|
|
if isinstance(self._json, dict):
|
|
return json_dumps(self._json).encode()
|
|
return self._json
|
|
|
|
def __init__(self, host, *, status_code=200, response=KASACAM_RESPONSE_ENCRYPTED):
|
|
self.host = host
|
|
self.http_client = HttpClient(DeviceConfig(self.host))
|
|
self.status_code = status_code
|
|
self.response = response
|
|
|
|
async def post(
|
|
self, url: URL, *, headers=None, params=None, json=None, data=None, **__
|
|
):
|
|
return self._mock_response(self.status_code, self.response)
|