feat(api): HTTP-Client fuer die ninlight-Lampen
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""HTTP client against a real local server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from custom_components.ninlight.api import (
|
||||
NinlightAuthError,
|
||||
NinlightBusyError,
|
||||
NinlightClient,
|
||||
NinlightConnectionError,
|
||||
NinlightRequestError,
|
||||
NinlightResponseError,
|
||||
)
|
||||
from tests_unit.conftest import FakeLamp
|
||||
from tests_unit.samples import INFO_RGB, SETTINGS_COMPILED, STATE_RGB, TOKEN
|
||||
|
||||
|
||||
def client_for(lamp: FakeLamp, session: aiohttp.ClientSession, **kwargs: float) -> NinlightClient:
|
||||
return NinlightClient(session, lamp.host, TOKEN, port=lamp.port, **kwargs)
|
||||
|
||||
|
||||
async def test_get_state_sends_raw_token_header(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/device/state", payload=STATE_RGB)
|
||||
state = await client_for(lamp, session).get_state()
|
||||
assert state == STATE_RGB
|
||||
call = lamp.calls[-1]
|
||||
assert call.headers["Authorization"] == TOKEN # no "Bearer"
|
||||
|
||||
|
||||
async def test_set_state_posts_fields_and_returns_lamp_answer(lamp: FakeLamp, session) -> None:
|
||||
answer = {**STATE_RGB, "brightness": 55} # lamp answer differs from the request
|
||||
lamp.reply("POST", "/api/v1/device/state", payload=answer)
|
||||
result = await client_for(lamp, session).set_state(brightness=78, transition=2500)
|
||||
assert result == answer
|
||||
assert lamp.calls[-1].body == {"brightness": 78, "transition": 2500}
|
||||
|
||||
|
||||
async def test_extra_fields_are_kept(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/device/state", payload={**STATE_RGB, "future_field": 7})
|
||||
assert (await client_for(lamp, session).get_state())["future_field"] == 7
|
||||
|
||||
|
||||
async def test_info_and_settings(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/system/info", payload=INFO_RGB)
|
||||
lamp.reply("GET", "/api/v1/system/settings", payload=SETTINGS_COMPILED)
|
||||
client = client_for(lamp, session)
|
||||
assert await client.get_info() == INFO_RGB
|
||||
assert await client.get_settings() == SETTINGS_COMPILED
|
||||
|
||||
|
||||
async def test_commands_use_nested_payload(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("PUT", "/api/v1/system/command", payload={"ok": True})
|
||||
client = client_for(lamp, session)
|
||||
await client.reboot()
|
||||
assert lamp.calls[-1].body == {"command": {"type": "reboot"}}
|
||||
lamp.reply("PUT", "/api/v1/system/command", status=202, payload={"status": "updating"})
|
||||
await client.fw_update("http://192.168.1.50:8123/api/ninlight/firmware/k3y")
|
||||
assert lamp.calls[-1].body == {
|
||||
"command": {
|
||||
"type": "fw_update",
|
||||
"url": "http://192.168.1.50:8123/api/ninlight/firmware/k3y",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def test_401_is_auth_error(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/system/settings", status=401, payload={"error": "unauthorized"})
|
||||
with pytest.raises(NinlightAuthError):
|
||||
await client_for(lamp, session).get_settings()
|
||||
|
||||
|
||||
async def test_409_is_busy_error_with_message(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply(
|
||||
"PUT",
|
||||
"/api/v1/system/command",
|
||||
status=409,
|
||||
payload={"error": "an update is already running"},
|
||||
)
|
||||
with pytest.raises(NinlightBusyError) as err:
|
||||
await client_for(lamp, session).fw_update("http://192.168.1.50:8123/x")
|
||||
assert err.value.status == 409
|
||||
assert err.value.message == "an update is already running"
|
||||
|
||||
|
||||
async def test_400_is_request_error_not_busy(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("POST", "/api/v1/device/state", status=400, payload={"error": "invalid brightness"})
|
||||
with pytest.raises(NinlightRequestError) as err:
|
||||
await client_for(lamp, session).set_state(brightness=0)
|
||||
assert not isinstance(err.value, NinlightBusyError)
|
||||
assert (err.value.status, err.value.message) == (400, "invalid brightness")
|
||||
|
||||
|
||||
async def test_error_body_without_json_uses_raw_text(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("POST", "/api/v1/device/state", status=500, raw="boom")
|
||||
with pytest.raises(NinlightRequestError) as err:
|
||||
await client_for(lamp, session).set_state(on=True)
|
||||
assert err.value.message == "boom"
|
||||
|
||||
|
||||
async def test_non_json_success_is_response_error(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/device/state", raw="<html>")
|
||||
with pytest.raises(NinlightResponseError):
|
||||
await client_for(lamp, session).get_state()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["on", "brightness", "mode", "capabilities"])
|
||||
async def test_state_without_required_field_is_response_error(
|
||||
lamp: FakeLamp, session, missing: str
|
||||
) -> None:
|
||||
lamp.reply(
|
||||
"GET", "/api/v1/device/state", payload={k: v for k, v in STATE_RGB.items() if k != missing}
|
||||
)
|
||||
with pytest.raises(NinlightResponseError):
|
||||
await client_for(lamp, session).get_state()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["mac", "device_type", "fw_version"])
|
||||
async def test_info_without_required_field_is_response_error(
|
||||
lamp: FakeLamp, session, missing: str
|
||||
) -> None:
|
||||
lamp.reply(
|
||||
"GET", "/api/v1/system/info", payload={k: v for k, v in INFO_RGB.items() if k != missing}
|
||||
)
|
||||
with pytest.raises(NinlightResponseError):
|
||||
await client_for(lamp, session).get_info()
|
||||
|
||||
|
||||
async def test_timeout_is_connection_error(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/device/state", payload=STATE_RGB, delay=0.5)
|
||||
with pytest.raises(NinlightConnectionError):
|
||||
await client_for(lamp, session, timeout=0.05).get_state()
|
||||
|
||||
|
||||
async def test_refused_connection_is_connection_error(session) -> None:
|
||||
client = NinlightClient(session, "127.0.0.1", TOKEN, port=1)
|
||||
with pytest.raises(NinlightConnectionError):
|
||||
await client.get_state()
|
||||
|
||||
|
||||
async def test_host_property_returns_configured_host(session) -> None:
|
||||
client = NinlightClient(session, "192.168.2.22", TOKEN, port=3333)
|
||||
assert client.host == "192.168.2.22"
|
||||
|
||||
|
||||
async def test_command_with_empty_response_body_succeeds(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("PUT", "/api/v1/system/command", status=204, raw="")
|
||||
await client_for(lamp, session).reboot()
|
||||
|
||||
|
||||
async def test_error_body_json_without_error_field_uses_raw_text(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("POST", "/api/v1/device/state", status=500, payload={"detail": "bad brightness"})
|
||||
with pytest.raises(NinlightRequestError) as err:
|
||||
await client_for(lamp, session).set_state(on=True)
|
||||
assert "detail" in err.value.message
|
||||
|
||||
|
||||
async def test_success_json_array_is_response_error(lamp: FakeLamp, session) -> None:
|
||||
lamp.reply("GET", "/api/v1/device/state", raw="[1, 2, 3]")
|
||||
with pytest.raises(NinlightResponseError):
|
||||
await client_for(lamp, session).get_state()
|
||||
Reference in New Issue
Block a user