cc9c010002
- Move aiomqtt to module-level import (enables proper patch() in tests) - Remove unused variable (description) in zigbee_service — ruff F841 - Split long line (132 chars) to fit 120 char limit — ruff E501 - Fix import sort order in test files — ruff I001 - Remove unused imports (asyncio, AsyncMock, MagicMock) — ruff F401 - Rename test_mqtt_connection import alias to _test_mqtt_connection to avoid pytest fixture name collision (ERROR at setup) - All 33 backend tests now pass (21 service + 12 router) - TypeScript typecheck: 0 errors Co-authored-by: CyberKeys <noreply@openclaw.ai>
229 lines
7.4 KiB
Python
229 lines
7.4 KiB
Python
"""API endpoint tests for /api/v1/zigbee/*."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture
|
|
async def headers(client: AsyncClient):
|
|
res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"})
|
|
token = res.json()["access_token"]
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# /api/v1/zigbee/test-connection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_success(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.test_mqtt_connection") as mock_conn:
|
|
mock_conn.return_value = True
|
|
res = await client.post(
|
|
"/api/v1/zigbee/test-connection",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert data["connected"] is True
|
|
assert "success" in data["message"].lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_failure(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.test_mqtt_connection") as mock_conn:
|
|
mock_conn.side_effect = ConnectionError("Connection refused")
|
|
res = await client.post(
|
|
"/api/v1/zigbee/test-connection",
|
|
json={"mqtt_host": "bad-host", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert data["connected"] is False
|
|
assert "refused" in data["message"].lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_requires_auth(client: AsyncClient) -> None:
|
|
res = await client.post(
|
|
"/api/v1/zigbee/test-connection",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
)
|
|
assert res.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_invalid_port(client: AsyncClient, headers: dict) -> None:
|
|
res = await client.post(
|
|
"/api/v1/zigbee/test-connection",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 99999},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 422 # pydantic validation error
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# /api/v1/zigbee/import
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_SAMPLE_NODES = [
|
|
{
|
|
"id": "0x00000000",
|
|
"label": "Coordinator",
|
|
"type": "zigbee_coordinator",
|
|
"ieee_address": "0x00000000",
|
|
"friendly_name": "Coordinator",
|
|
"device_type": "Coordinator",
|
|
"model": None,
|
|
"vendor": None,
|
|
"lqi": None,
|
|
"parent_id": None,
|
|
},
|
|
{
|
|
"id": "0x00000001",
|
|
"label": "router_1",
|
|
"type": "zigbee_router",
|
|
"ieee_address": "0x00000001",
|
|
"friendly_name": "router_1",
|
|
"device_type": "Router",
|
|
"model": "CC2530",
|
|
"vendor": "Texas Instruments",
|
|
"lqi": 230,
|
|
"parent_id": "0x00000000",
|
|
},
|
|
]
|
|
|
|
_SAMPLE_EDGES = [
|
|
{"source": "0x00000000", "target": "0x00000001"},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_success(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.return_value = (_SAMPLE_NODES, _SAMPLE_EDGES)
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={
|
|
"mqtt_host": "localhost",
|
|
"mqtt_port": 1883,
|
|
"base_topic": "zigbee2mqtt",
|
|
},
|
|
headers=headers,
|
|
)
|
|
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert data["device_count"] == 2
|
|
assert len(data["nodes"]) == 2
|
|
assert len(data["edges"]) == 1
|
|
coordinator = next(n for n in data["nodes"] if n["type"] == "zigbee_coordinator")
|
|
assert coordinator["ieee_address"] == "0x00000000"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_with_credentials(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.return_value = ([], [])
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={
|
|
"mqtt_host": "localhost",
|
|
"mqtt_port": 1883,
|
|
"mqtt_username": "admin",
|
|
"mqtt_password": "secret",
|
|
"base_topic": "z2m",
|
|
},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 200
|
|
mock_fetch.assert_called_once_with(
|
|
mqtt_host="localhost",
|
|
mqtt_port=1883,
|
|
base_topic="z2m",
|
|
username="admin",
|
|
password="secret",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_connection_error_returns_502(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.side_effect = ConnectionError("broker unreachable")
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_host": "bad-host", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 502
|
|
assert "broker unreachable" in res.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_timeout_returns_504(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.side_effect = TimeoutError("timed out")
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 504
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_malformed_payload_returns_422(client: AsyncClient, headers: dict) -> None:
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.side_effect = ValueError("malformed response")
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_requires_auth(client: AsyncClient) -> None:
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
)
|
|
assert res.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_empty_network(client: AsyncClient, headers: dict) -> None:
|
|
"""An empty Zigbee network (coordinator only) is a valid response."""
|
|
with patch("app.api.routes.zigbee.fetch_networkmap") as mock_fetch:
|
|
mock_fetch.return_value = ([], [])
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_host": "localhost", "mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 200
|
|
data = res.json()
|
|
assert data["device_count"] == 0
|
|
assert data["nodes"] == []
|
|
assert data["edges"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_missing_mqtt_host(client: AsyncClient, headers: dict) -> None:
|
|
res = await client.post(
|
|
"/api/v1/zigbee/import",
|
|
json={"mqtt_port": 1883},
|
|
headers=headers,
|
|
)
|
|
assert res.status_code == 422
|