Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/demetriek/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,9 +687,12 @@ async def notification_queue(self) -> list[Notification]:
try:
notifications.append(self._parse(Notification, notification))
except LaMetricError:
notification_id = (
notification.get("id") if isinstance(notification, dict) else None
)
_LOGGER.warning(
"Skipping notification %s, its format is not supported",
notification.get("id"),
notification_id,
)
return notifications

Expand Down
40 changes: 34 additions & 6 deletions tests/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,12 @@ async def test_invalid_json_response(
"""Test a broken JSON response raises a LaMetricError, without retrying."""
responses.get(f"{CLOUD_URL}/", status=200, body="{", repeat=True)

with pytest.raises(LaMetricError, match="invalid JSON"):
with pytest.raises(LaMetricError, match="invalid JSON") as error:
await cloud._request("/")

# Not a subclass, broken JSON is no reason to ask for new credentials.
assert error.type is LaMetricError

assert len(next(iter(responses.requests.values()))) == 1


Expand All @@ -183,18 +186,43 @@ async def test_get_current_user(responses: aioresponses, cloud: LaMetricCloud) -
assert User.from_dict(user.to_dict()) == user


@pytest.mark.parametrize(
("body", "match"),
[
('{"id": 1}', 'Field "apps_count" of type int is missing'),
(
(
'{"id": 1, "apps_count": "many", "email": "", "name": "",'
' "private_apps_count": 0, "private_device_count": 0}'
),
'Field "apps_count" of type int in User has invalid value',
),
],
)
async def test_get_current_user_unexpected_data(
responses: aioresponses, cloud: LaMetricCloud
responses: aioresponses, cloud: LaMetricCloud, body: str, match: str
) -> None:
"""Test data the library does not understand raises a LaMetricError."""
responses.get(f"{CLOUD_URL}/api/v2/users/me", status=200, body='{"id": 1}')
responses.get(f"{CLOUD_URL}/api/v2/users/me", status=200, body=body)

with pytest.raises(
LaMetricError, match='Field "apps_count" of type int is missing'
):
with pytest.raises(LaMetricError, match=match):
await cloud.current_user()


async def test_get_devices_unexpected_data(
responses: aioresponses, cloud: LaMetricCloud
) -> None:
"""Test a device the library does not understand raises a LaMetricError."""
responses.get(f"{CLOUD_URL}/api/v2/users/me/devices", status=200, body="[{}]")
responses.get(f"{CLOUD_URL}/api/v2/users/me/devices/42", status=200, body="{}")

with pytest.raises(LaMetricError, match="data this library does not understand"):
await cloud.devices()

with pytest.raises(LaMetricError, match="data this library does not understand"):
await cloud.device(device_id=42)


async def test_get_devices(responses: aioresponses, cloud: LaMetricCloud) -> None:
"""Test getting devices from the logged in account."""
responses.get(
Expand Down
70 changes: 70 additions & 0 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
NotificationSoundCategory,
Simple,
Sound,
Update,
)
from demetriek.const import (
NotificationType,
Expand Down Expand Up @@ -97,6 +98,60 @@ async def test_api_not_an_object(
await device.api()


@pytest.mark.parametrize(
("call", "path", "body"),
[
("display", "/api/v2/device/display", "{}"),
("audio", "/api/v2/device/audio", '{"volume": "loud"}'),
("bluetooth", "/api/v2/device/bluetooth", "{}"),
("wifi", "/api/v2/device/wifi", "{}"),
("apps", "/api/v2/device/apps", '{"com.lametric.clock": {}}'),
("stream", "/api/v2/device/stream", "{}"),
("notification_current", "/api/v2/device/notifications/current", '{"id": 1}'),
],
)
async def test_unexpected_data(
responses: aioresponses,
device: LaMetricDevice,
call: str,
path: str,
body: str,
) -> None:
"""Test every endpoint raises a LaMetricError on data it does not understand."""
responses.get(f"{DEVICE_URL}{path}", status=200, body=body)

with pytest.raises(LaMetricError, match="data this library does not understand"):
await getattr(device, call)()


async def test_get_device_wifi_not_an_object(
responses: aioresponses, device: LaMetricDevice
) -> None:
"""Test a Wi-Fi block that is not an object raises a LaMetricError."""
data = json.loads(load_fixture("device.json"))
data["wifi"] = "unexpected"
responses.get(f"{DEVICE_URL}/api/v2/device", status=200, body=json.dumps(data))

with pytest.raises(LaMetricError, match='Field "wifi"'):
await device.device()


async def test_get_device_update(
responses: aioresponses, device: LaMetricDevice
) -> None:
"""Test an available firmware update is exposed as an Update."""
responses.get(
f"{DEVICE_URL}/api/v2/device",
status=200,
body=load_fixture("device_sa5_1.json"),
)

result = await device.device()

assert isinstance(result.update, Update)
assert result.update.version == "3.2.1"


async def test_notify(responses: aioresponses, device: LaMetricDevice) -> None:
"""Test sending notification serialization."""
url = f"{DEVICE_URL}/api/v2/device/notifications"
Expand Down Expand Up @@ -389,6 +444,21 @@ async def test_notification_queue_skips_unsupported(
assert "Skipping notification 26" in caplog.text


async def test_notification_queue_skips_null(
responses: aioresponses, device: LaMetricDevice
) -> None:
"""Test an empty entry in the queue does not break the whole queue."""
responses.get(
f"{DEVICE_URL}/api/v2/device/notifications",
status=200,
body='[null, {"id": "25", "model": {"frames": [{"text": "first"}]}}]',
)

notifications = await device.notification_queue()

assert [notification.notification_id for notification in notifications] == [25]


async def test_dismiss_all_notifications_unsupported(
responses: aioresponses, device: LaMetricDevice
) -> None:
Expand Down
5 changes: 4 additions & 1 deletion tests/test_lametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,12 @@ async def test_invalid_json_response(
"""Test a broken JSON response raises a LaMetricError, without retrying."""
responses.get(f"{DEVICE_URL}/", status=200, body="{", repeat=True)

with pytest.raises(LaMetricError, match="invalid JSON"):
with pytest.raises(LaMetricError, match="invalid JSON") as error:
await device._request("/")

# Not a subclass, broken JSON is no reason to ask for new credentials.
assert error.type is LaMetricError

assert len(next(iter(responses.requests.values()))) == 1


Expand Down
13 changes: 11 additions & 2 deletions tests/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ def test_build_lmsp_packet_encoded() -> None:
[StreamArea(data=b"", width=-1, height=0)],
"width must be between 0 and 65535, got -1",
),
(
[StreamArea(data=b"", width=0, height=-1)],
"height must be between 0 and 65535, got -1",
),
],
)
def test_build_lmsp_packet_invalid(areas: list[StreamArea], match: str) -> None:
Expand Down Expand Up @@ -256,11 +260,16 @@ async def test_stream_send() -> None:
assert packet == DOCUMENTED_HEADER + frame


async def test_stream_connect_error(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.parametrize(
"exception", [socket.gaierror(), OSError("Socket unavailable")]
)
async def test_stream_connect_error(
monkeypatch: pytest.MonkeyPatch, exception: OSError
) -> None:
"""Test a socket that cannot be opened raises a connection error."""

async def fail(*_args: object, **_kwargs: object) -> None:
raise socket.gaierror
raise exception

monkeypatch.setattr(asyncio.get_running_loop(), "create_datagram_endpoint", fail)
stream = LaMetricStream(host="lametric.invalid", session=_session(9999))
Expand Down
Loading