Files

163 lines
6.5 KiB
Python

"""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()