feat: update-Entitaet fuer die Firmware und das Testgeruest
Aufgebaut wie ha-ninlight: die Abbilder liegen unter ninstrip_firmware im Konfigurationsverzeichnis, Home Assistant liefert sie selbst unter einer einmaligen Adresse aus, und zwar genau die Bytes, die vorher geprueft wurden. Dazu firmware.py (Anhaenger, Versionsreihenfolge, WLAN-Pruefung, Katalog), http.py, urls.py und shared.py. Drei Unterschiede zu ninlight, alle aus der Firmware heraus: * Verglichen wird device_type - die Platine -, nicht product. Letzteres ist bei allen Platinen 'ninstrip'; die Abbilder unterscheiden sich aber hart, die K1Y invertiert das Datensignal und die KL-RGBC3 nicht. Firmware ohne dieses Feld bekommt gar kein Angebot. * Die WLAN-Pruefung sucht die SSID unverpackt: ein ESP32-Abbild ist nicht gzip-verpackt wie das des W600. An einem mit sdkconfig.local gebauten Abbild gegengeprueft - die SSID steht genau einmal NUL-gerahmt drin. * Der Fehlerschluessel heisst device_url_too_long statt lamp_url_too_long. Dazu das Testgeruest, das es hier bisher gar nicht gab: 204 Tests in tests_unit/ (ohne HA-Instanz) und tests/ (mit), Abdeckung 97 %, dazu scripts/check.sh als Ersatz fuer die fehlende CI. Die Nutzdaten in tests_unit/samples.py sind an der Stehlampe abgefragt. Beim Gegenpruefen per Mutation fiel auf, dass die Platinenpruefung unmittelbar vor dem Senden ungeprueft war - der Test dazu ist jetzt da und faellt, wenn man die Pruefung entfernt.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""HTTP-Client gegen einen echten lokalen Server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from custom_components.ninstrip.api import (
|
||||
NinstripAuthError,
|
||||
NinstripBusyError,
|
||||
NinstripClient,
|
||||
NinstripConnectionError,
|
||||
NinstripRequestError,
|
||||
NinstripResponseError,
|
||||
)
|
||||
from tests_unit.conftest import FakeStrip
|
||||
from tests_unit.samples import INFO_K1Y, PRESETS_K1Y, SETTINGS_COMPILED, STATE_K1Y, TOKEN
|
||||
|
||||
|
||||
def client_for(strip: FakeStrip, session: aiohttp.ClientSession, **kwargs: float) -> NinstripClient:
|
||||
return NinstripClient(session, strip.host, TOKEN, port=strip.port, **kwargs)
|
||||
|
||||
|
||||
async def test_zustand_schickt_das_token_roh(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/device/state", payload=STATE_K1Y)
|
||||
zustand = await client_for(strip, session).get_state()
|
||||
assert zustand == STATE_K1Y
|
||||
assert strip.calls[-1].headers["Authorization"] == TOKEN # kein "Bearer"
|
||||
|
||||
|
||||
async def test_set_state_schickt_felder_und_gibt_die_antwort_zurueck(
|
||||
strip: FakeStrip, session
|
||||
) -> None:
|
||||
antwort = {**STATE_K1Y, "brightness": 55} # die Antwort weicht von der Anfrage ab
|
||||
strip.reply("POST", "/api/v1/device/state", payload=antwort)
|
||||
ergebnis = await client_for(strip, session).set_state(brightness=78, transition=2500)
|
||||
assert ergebnis == antwort
|
||||
assert strip.calls[-1].body == {"brightness": 78, "transition": 2500}
|
||||
|
||||
|
||||
async def test_unbekannte_felder_bleiben_erhalten(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/device/state", payload={**STATE_K1Y, "kuenftig": 7})
|
||||
assert (await client_for(strip, session).get_state())["kuenftig"] == 7
|
||||
|
||||
|
||||
async def test_info_settings_und_presets(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/system/info", payload=INFO_K1Y)
|
||||
strip.reply("GET", "/api/v1/system/settings", payload=SETTINGS_COMPILED)
|
||||
strip.reply("GET", "/api/v1/device/presets", payload=PRESETS_K1Y)
|
||||
client = client_for(strip, session)
|
||||
assert await client.get_info() == INFO_K1Y
|
||||
assert await client.get_settings() == SETTINGS_COMPILED
|
||||
assert await client.get_presets() == PRESETS_K1Y
|
||||
|
||||
|
||||
async def test_sacn_wird_als_settings_typ_geschrieben(strip: FakeStrip, session) -> None:
|
||||
strip.reply("PUT", "/api/v1/system/settings", payload={"status": "ok"})
|
||||
await client_for(strip, session).set_sacn(universe=7, start_slot=4)
|
||||
assert strip.calls[-1].body == {"type": "sacn", "universe": 7, "start_slot": 4}
|
||||
|
||||
|
||||
async def test_szenen_anwenden_und_sichern(strip: FakeStrip, session) -> None:
|
||||
strip.reply("PUT", "/api/v1/device/presets/3/apply", payload={"status": "ok"})
|
||||
strip.reply("PUT", "/api/v1/device/presets/3", payload={"status": "ok"})
|
||||
client = client_for(strip, session)
|
||||
await client.apply_preset(3)
|
||||
assert strip.calls[-1].path == "/api/v1/device/presets/3/apply"
|
||||
await client.save_preset(3, "Abend")
|
||||
assert strip.calls[-1].body == {"name": "Abend"}
|
||||
await client.save_preset(3)
|
||||
assert strip.calls[-1].body == {}
|
||||
|
||||
|
||||
async def test_befehle_liegen_verschachtelt(strip: FakeStrip, session) -> None:
|
||||
strip.reply("PUT", "/api/v1/system/command", payload={"ok": True})
|
||||
client = client_for(strip, session)
|
||||
await client.reboot()
|
||||
assert strip.calls[-1].body == {"command": {"type": "reboot"}}
|
||||
strip.reply("PUT", "/api/v1/system/command", status=202, payload={"status": "updating"})
|
||||
await client.fw_update("http://192.168.1.50:8123/api/ninstrip/firmware/k3y")
|
||||
assert strip.calls[-1].body == {
|
||||
"command": {
|
||||
"type": "fw_update",
|
||||
"url": "http://192.168.1.50:8123/api/ninstrip/firmware/k3y",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def test_401_ist_ein_auth_fehler(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/system/settings", status=401, payload={"error": "Token fehlt"})
|
||||
with pytest.raises(NinstripAuthError):
|
||||
await client_for(strip, session).get_settings()
|
||||
|
||||
|
||||
async def test_409_ist_beschaeftigt_mit_meldung(strip: FakeStrip, session) -> None:
|
||||
strip.reply(
|
||||
"PUT", "/api/v1/system/command", status=409, payload={"error": "Update laeuft schon"}
|
||||
)
|
||||
with pytest.raises(NinstripBusyError) as err:
|
||||
await client_for(strip, session).fw_update("http://192.168.1.50:8123/x")
|
||||
assert err.value.status == 409
|
||||
assert err.value.message == "Update laeuft schon"
|
||||
|
||||
|
||||
async def test_400_ist_ein_anfragefehler_kein_beschaeftigt(strip: FakeStrip, session) -> None:
|
||||
strip.reply("POST", "/api/v1/device/state", status=400, payload={"error": "brightness"})
|
||||
with pytest.raises(NinstripRequestError) as err:
|
||||
await client_for(strip, session).set_state(brightness=0)
|
||||
assert not isinstance(err.value, NinstripBusyError)
|
||||
assert (err.value.status, err.value.message) == (400, "brightness")
|
||||
|
||||
|
||||
async def test_fehlertext_ohne_json_kommt_roh(strip: FakeStrip, session) -> None:
|
||||
strip.reply("POST", "/api/v1/device/state", status=500, raw="peng")
|
||||
with pytest.raises(NinstripRequestError) as err:
|
||||
await client_for(strip, session).set_state(on=True)
|
||||
assert err.value.message == "peng"
|
||||
|
||||
|
||||
async def test_json_fehlertext_ohne_error_feld_kommt_roh(strip: FakeStrip, session) -> None:
|
||||
strip.reply("POST", "/api/v1/device/state", status=500, payload={"detail": "zu hell"})
|
||||
with pytest.raises(NinstripRequestError) as err:
|
||||
await client_for(strip, session).set_state(on=True)
|
||||
assert "detail" in err.value.message
|
||||
|
||||
|
||||
async def test_erfolg_ohne_json_ist_ein_antwortfehler(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/device/state", raw="<html>")
|
||||
with pytest.raises(NinstripResponseError):
|
||||
await client_for(strip, session).get_state()
|
||||
|
||||
|
||||
async def test_json_array_als_erfolg_ist_ein_antwortfehler(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/device/state", raw="[1, 2, 3]")
|
||||
with pytest.raises(NinstripResponseError):
|
||||
await client_for(strip, session).get_state()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fehlt", ["on", "brightness", "segment_count", "pixel_count"])
|
||||
async def test_zustand_ohne_pflichtfeld_ist_ein_antwortfehler(
|
||||
strip: FakeStrip, session, fehlt: str
|
||||
) -> None:
|
||||
strip.reply(
|
||||
"GET", "/api/v1/device/state", payload={k: v for k, v in STATE_K1Y.items() if k != fehlt}
|
||||
)
|
||||
with pytest.raises(NinstripResponseError):
|
||||
await client_for(strip, session).get_state()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fehlt", ["mac", "product", "fw_version"])
|
||||
async def test_info_ohne_pflichtfeld_ist_ein_antwortfehler(
|
||||
strip: FakeStrip, session, fehlt: str
|
||||
) -> None:
|
||||
strip.reply(
|
||||
"GET", "/api/v1/system/info", payload={k: v for k, v in INFO_K1Y.items() if k != fehlt}
|
||||
)
|
||||
with pytest.raises(NinstripResponseError):
|
||||
await client_for(strip, session).get_info()
|
||||
|
||||
|
||||
async def test_zeitueberschreitung_ist_ein_verbindungsfehler(strip: FakeStrip, session) -> None:
|
||||
strip.reply("GET", "/api/v1/device/state", payload=STATE_K1Y, delay=0.5)
|
||||
with pytest.raises(NinstripConnectionError):
|
||||
await client_for(strip, session, timeout=0.05).get_state()
|
||||
|
||||
|
||||
async def test_abgewiesene_verbindung_ist_ein_verbindungsfehler(session) -> None:
|
||||
client = NinstripClient(session, "127.0.0.1", TOKEN, port=1)
|
||||
with pytest.raises(NinstripConnectionError):
|
||||
await client.get_state()
|
||||
|
||||
|
||||
async def test_host_gibt_die_eingestellte_adresse(session) -> None:
|
||||
assert NinstripClient(session, "192.168.2.238", TOKEN, port=3333).host == "192.168.2.238"
|
||||
|
||||
|
||||
async def test_befehl_ohne_antwortkoerper_geht_durch(strip: FakeStrip, session) -> None:
|
||||
strip.reply("PUT", "/api/v1/system/command", status=204, raw="")
|
||||
await client_for(strip, session).reboot()
|
||||
Reference in New Issue
Block a user