M5-T08: add MQTT, HA Discovery, and Modbus polling config fields
This commit is contained in:
@@ -410,3 +410,150 @@ def test_smtp_test_response_does_not_echo_smtp_password(client: TestClient) -> N
|
||||
assert "message" in body
|
||||
# The plaintext password must not appear anywhere in the response body
|
||||
assert smtp_password not in response.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# M5-T08: MQTT / HA Discovery / Modbus CONFIG_FIELDS
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_config_includes_mqtt_section(client: TestClient) -> None:
|
||||
"""GET /api/config must include an MQTT section with expected fields."""
|
||||
_login(client)
|
||||
|
||||
response = client.get("/api/config")
|
||||
body = response.json()
|
||||
|
||||
section_names = {s["name"] for s in body["sections"]}
|
||||
assert "MQTT" in section_names, f"MQTT section missing; got {section_names}"
|
||||
|
||||
mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
|
||||
env_names = {f["env_name"] for f in mqtt_section["fields"]}
|
||||
assert "MQTT_ENABLED" in env_names
|
||||
assert "MQTT_BROKER_HOST" in env_names
|
||||
assert "MQTT_BROKER_PORT" in env_names
|
||||
assert "MQTT_USERNAME" in env_names
|
||||
assert "MQTT_PASSWORD" in env_names
|
||||
assert "MQTT_TLS_ENABLED" in env_names
|
||||
|
||||
|
||||
def test_get_config_includes_ha_discovery_section(client: TestClient) -> None:
|
||||
"""GET /api/config must include a Home Assistant Discovery section."""
|
||||
_login(client)
|
||||
|
||||
response = client.get("/api/config")
|
||||
body = response.json()
|
||||
|
||||
section_names = {s["name"] for s in body["sections"]}
|
||||
assert "Home Assistant Discovery" in section_names, (
|
||||
f"Home Assistant Discovery section missing; got {section_names}"
|
||||
)
|
||||
|
||||
ha_section = next(s for s in body["sections"] if s["name"] == "Home Assistant Discovery")
|
||||
env_names = {f["env_name"] for f in ha_section["fields"]}
|
||||
assert "HA_DISCOVERY_ENABLED" in env_names
|
||||
assert "HA_DISCOVERY_PREFIX" in env_names
|
||||
|
||||
|
||||
def test_get_config_includes_modbus_section(client: TestClient) -> None:
|
||||
"""GET /api/config must include a Modbus section with MODBUS_POLLING_ENABLED."""
|
||||
_login(client)
|
||||
|
||||
response = client.get("/api/config")
|
||||
body = response.json()
|
||||
|
||||
section_names = {s["name"] for s in body["sections"]}
|
||||
assert "Modbus" in section_names, f"Modbus section missing; got {section_names}"
|
||||
|
||||
modbus_section = next(s for s in body["sections"] if s["name"] == "Modbus")
|
||||
env_names = {f["env_name"] for f in modbus_section["fields"]}
|
||||
assert "MODBUS_POLLING_ENABLED" in env_names
|
||||
|
||||
|
||||
def test_get_config_mqtt_password_is_secret(client: TestClient) -> None:
|
||||
"""MQTT_PASSWORD must be marked secret and return empty string in GET response."""
|
||||
_login(client)
|
||||
|
||||
response = client.get("/api/config")
|
||||
body = response.json()
|
||||
|
||||
mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
|
||||
password_field = next(f for f in mqtt_section["fields"] if f["env_name"] == "MQTT_PASSWORD")
|
||||
|
||||
assert password_field["secret"] is True
|
||||
assert password_field["value"] == "", (
|
||||
f"MQTT_PASSWORD should be masked (empty string), got {password_field['value']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_mqtt_broker_port_input_type_is_number(client: TestClient) -> None:
|
||||
"""MQTT_BROKER_PORT must have input_type='number' for correct frontend rendering."""
|
||||
_login(client)
|
||||
|
||||
response = client.get("/api/config")
|
||||
body = response.json()
|
||||
|
||||
mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
|
||||
port_field = next(f for f in mqtt_section["fields"] if f["env_name"] == "MQTT_BROKER_PORT")
|
||||
|
||||
assert port_field["input_type"] == "number", (
|
||||
f"MQTT_BROKER_PORT input_type should be 'number', got {port_field['input_type']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_put_config_blank_mqtt_password_keeps_existing(
|
||||
client: TestClient, test_database_urls
|
||||
) -> None:
|
||||
"""Submitting blank MQTT_PASSWORD must NOT overwrite the stored secret."""
|
||||
_login(client)
|
||||
|
||||
# Store a secret via full PUT
|
||||
payload_with_secret = _full_config_payload({"MQTT_PASSWORD": "mqtt-secret-value"})
|
||||
resp1 = client.put(
|
||||
"/api/config",
|
||||
json={"updates": payload_with_secret},
|
||||
headers={"X-CSRF-Token": "token"},
|
||||
)
|
||||
assert resp1.status_code == 200, f"First PUT failed: {resp1.status_code} {resp1.text}"
|
||||
|
||||
# PUT with blank for that secret (keep-old semantics)
|
||||
payload_blank_secret = _full_config_payload({"MQTT_PASSWORD": ""})
|
||||
resp2 = client.put(
|
||||
"/api/config",
|
||||
json={"updates": payload_blank_secret},
|
||||
headers={"X-CSRF-Token": "token"},
|
||||
)
|
||||
assert resp2.status_code == 200, f"Second PUT failed: {resp2.status_code} {resp2.text}"
|
||||
|
||||
# The stored value in the DB should still be the original secret
|
||||
conn = sqlite3.connect(test_database_urls["app_path"])
|
||||
try:
|
||||
rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert rows.get("MQTT_PASSWORD") == "mqtt-secret-value"
|
||||
|
||||
|
||||
def test_put_config_invalid_mqtt_port_returns_422_and_does_not_write(
|
||||
client: TestClient, test_database_urls
|
||||
) -> None:
|
||||
"""Non-numeric MQTT_BROKER_PORT must return 422 and not persist the bad value."""
|
||||
_login(client)
|
||||
|
||||
payload = _full_config_payload({"MQTT_BROKER_PORT": "not-a-number"})
|
||||
response = client.put(
|
||||
"/api/config",
|
||||
json={"updates": payload},
|
||||
headers={"X-CSRF-Token": "token"},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
conn = sqlite3.connect(test_database_urls["app_path"])
|
||||
try:
|
||||
rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert rows.get("MQTT_BROKER_PORT") != "not-a-number"
|
||||
|
||||
Reference in New Issue
Block a user