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