Files
KX-Bridge-Release/tests/test_settings.py
viewit 5eeed97514 fix(bridge): camera process-race and unhandled JSON errors in two handlers
Found during a targeted code review, not from a user report:

- _run_jpeg_loop()/_run_h264_loop() operated directly on the shared
  self._proc_jpeg/self._proc_h264 instance attributes in their cleanup,
  unlike _run_mjpeg_loop() (already fixed for exactly this) which uses a
  local `proc` reference. If a loop's task is cancelled - e.g. by
  CameraCache.reset() after the printer rotates its stream URL on reboot -
  while a new task has already started and assigned its own process to the
  shared attribute, the cancelled task's cleanup killed the NEWER process
  instead of its own, leaking its own ffmpeg child as an orphan. Applied
  the same local-variable + identity-check pattern already used by
  _run_mjpeg_loop.

- handle_api_settings_post and handle_api_update_apply were the only two
  of ~84 handlers that called `await request.json()` without a try/except -
  every other handler follows the established pattern of returning a clean
  400 for a malformed body. A trivial malformed request to either endpoint
  produced an unhandled 500 with a full traceback instead.

New tests in tests/test_camera_process_race.py,
tests/test_settings.py, and tests/test_update_check.py cover both.
2026-08-04 15:31:36 +02:00

96 lines
3.3 KiB
Python

"""
Tests für /api/settings — Lesen und Schreiben der Verbindungseinstellungen.
"""
import pytest
import tempfile
import pathlib
@pytest.mark.asyncio
async def test_settings_get_returns_200(client):
c, _ = client
resp = await c.get("/api/settings")
assert resp.status == 200
@pytest.mark.asyncio
async def test_settings_get_schema(client):
c, _ = client
data = await (await c.get("/api/settings")).json()
for key in ("printer_ip", "mqtt_port", "username", "password", "device_id", "mode_id"):
assert key in data
@pytest.mark.asyncio
async def test_settings_get_empty_when_unconfigured(client):
"""Frische Bridge ohne Zugangsdaten → printer_ip und device_id leer."""
c, _ = client
data = await (await c.get("/api/settings")).json()
assert data["printer_ip"] == ""
assert data["device_id"] == ""
@pytest.mark.asyncio
async def test_settings_get_returns_configured_values(client_configured):
"""Bridge mit Zugangsdaten → Werte korrekt zurückgegeben."""
c, _ = client_configured
data = await (await c.get("/api/settings")).json()
assert data["printer_ip"] == "192.168.1.100"
assert data["device_id"] == "abc123deadbeef"
@pytest.mark.asyncio
async def test_settings_post_invalid_json_returns_400(client):
"""A malformed/non-JSON body must be a clean 400, not an unhandled 500
with a raw JSONDecodeError traceback (code review finding)."""
c, _ = client
resp = await c.post("/api/settings", data=b"not json", headers={"Content-Type": "application/json"})
assert resp.status == 400
@pytest.mark.asyncio
async def test_settings_post_writes_config_ini(client):
"""POST /api/settings schreibt Werte in config.ini (Migration von .env, v0.9.x)."""
c, bridge = client
with tempfile.TemporaryDirectory() as tmpdir:
config_path = pathlib.Path(tmpdir) / "config.ini"
bridge._find_config_path = lambda: config_path
bridge._restart_bridge = lambda: None # POST triggers a restart — don't kill the test process
resp = await c.post("/api/settings", json={
"printer_ip": "10.0.0.5",
"mqtt_port": 9883,
"username": "userABCD",
"password": "secret123",
"device_id": "deadbeef01234567",
"mode_id": "20030",
})
assert resp.status == 200
content = config_path.read_text()
assert "printer_ip = 10.0.0.5" in content
assert "username = userABCD" in content
assert "device_id = deadbeef01234567" in content
@pytest.mark.asyncio
async def test_settings_post_preserves_existing_keys(client):
"""POST darf unbekannte Sections/Optionen in config.ini nicht löschen (z.B. Spoolman-Server)."""
c, bridge = client
with tempfile.TemporaryDirectory() as tmpdir:
config_path = pathlib.Path(tmpdir) / "config.ini"
config_path.write_text(
"[spoolman]\nserver = http://192.168.1.50:7912\n\n"
"[connection]\nprinter_ip = old\n"
)
bridge._find_config_path = lambda: config_path
bridge._restart_bridge = lambda: None
await c.post("/api/settings", json={"printer_ip": "10.0.0.99"})
content = config_path.read_text()
assert "server = http://192.168.1.50:7912" in content
assert "printer_ip = 10.0.0.99" in content