feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
323
backend/tests/integration/test_monitoring_api.py
Normal file
323
backend/tests/integration/test_monitoring_api.py
Normal file
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
Integration tests for the Monitoring / Metrics API endpoints.
|
||||
|
||||
Tests exercise:
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/health
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/interfaces
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/interfaces/list
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/wireless
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/wireless/latest
|
||||
- /api/tenants/{tenant_id}/devices/{device_id}/metrics/sparkline
|
||||
- /api/tenants/{tenant_id}/fleet/summary
|
||||
- /api/fleet/summary (super_admin only)
|
||||
|
||||
All tests run against real PostgreSQL+TimescaleDB.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
class TestHealthMetrics:
|
||||
"""Device health metrics endpoints."""
|
||||
|
||||
async def test_get_device_health_metrics_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET health metrics for a device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
start = (now - timedelta(hours=1)).isoformat()
|
||||
end = now.isoformat()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/health",
|
||||
params={"start": start, "end": end},
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 0
|
||||
|
||||
async def test_get_device_health_metrics_with_data(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET health metrics returns bucketed data when rows exist."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.flush()
|
||||
|
||||
# Insert test metric rows directly via admin session
|
||||
now = datetime.now(timezone.utc)
|
||||
for i in range(5):
|
||||
ts = now - timedelta(minutes=i * 5)
|
||||
await admin_session.execute(
|
||||
text(
|
||||
"INSERT INTO health_metrics "
|
||||
"(device_id, time, cpu_load, free_memory, total_memory, "
|
||||
"free_disk, total_disk, temperature) "
|
||||
"VALUES (:device_id, :ts, :cpu, :free_mem, :total_mem, "
|
||||
":free_disk, :total_disk, :temp)"
|
||||
),
|
||||
{
|
||||
"device_id": str(device.id),
|
||||
"ts": ts,
|
||||
"cpu": 30 + i * 5,
|
||||
"free_mem": 500000000,
|
||||
"total_mem": 1000000000,
|
||||
"free_disk": 200000000,
|
||||
"total_disk": 500000000,
|
||||
"temp": 45,
|
||||
},
|
||||
)
|
||||
await admin_session.commit()
|
||||
|
||||
start = (now - timedelta(hours=1)).isoformat()
|
||||
end = now.isoformat()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/health",
|
||||
params={"start": start, "end": end},
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
# Each bucket should have expected fields
|
||||
for bucket in data:
|
||||
assert "bucket" in bucket
|
||||
assert "avg_cpu" in bucket
|
||||
|
||||
|
||||
class TestInterfaceMetrics:
|
||||
"""Interface traffic metrics endpoints."""
|
||||
|
||||
async def test_get_interface_metrics_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET interface metrics for device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/interfaces",
|
||||
params={
|
||||
"start": (now - timedelta(hours=1)).isoformat(),
|
||||
"end": now.isoformat(),
|
||||
},
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
async def test_get_interface_list_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET interface list for device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/interfaces/list",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
|
||||
class TestSparkline:
|
||||
"""Sparkline endpoint."""
|
||||
|
||||
async def test_sparkline_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET sparkline for device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/sparkline",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
|
||||
class TestFleetSummary:
|
||||
"""Fleet summary endpoints."""
|
||||
|
||||
async def test_fleet_summary_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET /api/tenants/{tenant_id}/fleet/summary returns 200 with empty fleet."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/fleet/summary",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
async def test_fleet_summary_with_devices(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET fleet summary returns device data when devices exist."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
await create_test_device(admin_session, tenant.id, hostname="fleet-dev-1")
|
||||
await create_test_device(admin_session, tenant.id, hostname="fleet-dev-2")
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/fleet/summary",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 2
|
||||
hostnames = [d["hostname"] for d in data]
|
||||
assert "fleet-dev-1" in hostnames
|
||||
assert "fleet-dev-2" in hostnames
|
||||
|
||||
async def test_fleet_summary_unauthenticated(self, client):
|
||||
"""GET fleet summary without auth returns 401."""
|
||||
tenant_id = str(uuid.uuid4())
|
||||
resp = await client.get(f"/api/tenants/{tenant_id}/fleet/summary")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestWirelessMetrics:
|
||||
"""Wireless metrics endpoints."""
|
||||
|
||||
async def test_wireless_metrics_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET wireless metrics for device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/wireless",
|
||||
params={
|
||||
"start": (now - timedelta(hours=1)).isoformat(),
|
||||
"end": now.isoformat(),
|
||||
},
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
async def test_wireless_latest_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET wireless latest for device with no data returns 200 + empty list."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}/metrics/wireless/latest",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
Reference in New Issue
Block a user