157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
|
|
"""Integration test: verify metrics flow from WG clients → collector → VictoriaMetrics.
|
||
|
|
|
||
|
|
Requires the full integration stack running: make test-stack-up
|
||
|
|
Run with: make test-stack-verify (or: uv run pytest tests/integration/ -v)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
VM_URL = os.environ.get("WG_VICTORIAMETRICS_URL", "http://localhost:8428")
|
||
|
|
WIREGUI_URL = os.environ.get("WG_EXTERNAL_URL", "http://localhost:13000")
|
||
|
|
|
||
|
|
EXPECTED_CLIENTS = ["test-client-1", "test-client-2", "test-client-3"]
|
||
|
|
# Wait up to this long for metrics to appear (collector runs every 5s)
|
||
|
|
MAX_WAIT = 60
|
||
|
|
POLL_INTERVAL = 5
|
||
|
|
|
||
|
|
|
||
|
|
def _vm_query(query: str) -> dict:
|
||
|
|
"""Execute an instant query against VictoriaMetrics."""
|
||
|
|
resp = httpx.get(f"{VM_URL}/api/v1/query", params={"query": query}, timeout=5)
|
||
|
|
resp.raise_for_status()
|
||
|
|
return resp.json()
|
||
|
|
|
||
|
|
|
||
|
|
def _vm_series(metric: str) -> list[dict]:
|
||
|
|
"""Get all series for a metric from VictoriaMetrics."""
|
||
|
|
resp = httpx.get(f"{VM_URL}/api/v1/series", params={"match[]": metric}, timeout=5)
|
||
|
|
resp.raise_for_status()
|
||
|
|
return resp.json().get("data", [])
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(scope="module", autouse=True)
|
||
|
|
def check_stack_running():
|
||
|
|
"""Skip all tests if the integration stack isn't running."""
|
||
|
|
try:
|
||
|
|
r = httpx.get(f"{WIREGUI_URL}/api/health", timeout=3)
|
||
|
|
if r.status_code != 200:
|
||
|
|
pytest.skip("WireGUI not running")
|
||
|
|
except httpx.HTTPError:
|
||
|
|
pytest.skip("WireGUI not running — start with: make test-stack-up")
|
||
|
|
|
||
|
|
try:
|
||
|
|
r = httpx.get(f"{VM_URL}/health", timeout=3)
|
||
|
|
if r.status_code != 200:
|
||
|
|
pytest.skip("VictoriaMetrics not running")
|
||
|
|
except httpx.HTTPError:
|
||
|
|
pytest.skip("VictoriaMetrics not running — start with: make test-stack-up")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(scope="module")
|
||
|
|
def wait_for_metrics():
|
||
|
|
"""Wait until at least one peer metric appears in VictoriaMetrics."""
|
||
|
|
deadline = time.time() + MAX_WAIT
|
||
|
|
while time.time() < deadline:
|
||
|
|
result = _vm_query("wiregui_peers_total")
|
||
|
|
data = result.get("data", {}).get("result", [])
|
||
|
|
if data and float(data[0].get("value", [0, "0"])[1]) > 0:
|
||
|
|
return
|
||
|
|
time.sleep(POLL_INTERVAL)
|
||
|
|
pytest.fail(f"No metrics appeared in VictoriaMetrics after {MAX_WAIT}s")
|
||
|
|
|
||
|
|
|
||
|
|
def test_peers_total(wait_for_metrics):
|
||
|
|
"""wiregui_peers_total reports at least 1 active peer."""
|
||
|
|
result = _vm_query("wiregui_peers_total")
|
||
|
|
data = result["data"]["result"]
|
||
|
|
assert len(data) > 0
|
||
|
|
value = float(data[0]["value"][1])
|
||
|
|
assert value >= 1, f"Expected at least 1 peer, got {value}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_rx_bytes_per_client(wait_for_metrics):
|
||
|
|
"""Each client has wiregui_peer_rx_bytes > 0."""
|
||
|
|
series = _vm_series("wiregui_peer_rx_bytes")
|
||
|
|
device_names = {s.get("device_name") for s in series}
|
||
|
|
|
||
|
|
for client in EXPECTED_CLIENTS:
|
||
|
|
assert client in device_names, (
|
||
|
|
f"Missing rx_bytes metric for '{client}'. "
|
||
|
|
f"Found: {device_names}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Verify values are non-zero (traffic is flowing)
|
||
|
|
for client in EXPECTED_CLIENTS:
|
||
|
|
result = _vm_query(f'wiregui_peer_rx_bytes{{device_name="{client}"}}')
|
||
|
|
data = result["data"]["result"]
|
||
|
|
assert len(data) > 0, f"No rx_bytes data for {client}"
|
||
|
|
value = float(data[0]["value"][1])
|
||
|
|
assert value > 0, f"rx_bytes for {client} is 0 — no traffic?"
|
||
|
|
|
||
|
|
|
||
|
|
def test_tx_bytes_per_client(wait_for_metrics):
|
||
|
|
"""Each client has wiregui_peer_tx_bytes > 0."""
|
||
|
|
for client in EXPECTED_CLIENTS:
|
||
|
|
result = _vm_query(f'wiregui_peer_tx_bytes{{device_name="{client}"}}')
|
||
|
|
data = result["data"]["result"]
|
||
|
|
assert len(data) > 0, f"No tx_bytes data for {client}"
|
||
|
|
value = float(data[0]["value"][1])
|
||
|
|
assert value > 0, f"tx_bytes for {client} is 0 — no traffic?"
|
||
|
|
|
||
|
|
|
||
|
|
def test_handshake_per_client(wait_for_metrics):
|
||
|
|
"""Each client has a recent handshake timestamp."""
|
||
|
|
now = time.time()
|
||
|
|
for client in EXPECTED_CLIENTS:
|
||
|
|
result = _vm_query(f'wiregui_peer_latest_handshake_seconds{{device_name="{client}"}}')
|
||
|
|
data = result["data"]["result"]
|
||
|
|
assert len(data) > 0, f"No handshake data for {client}"
|
||
|
|
ts = float(data[0]["value"][1])
|
||
|
|
assert ts > 0, f"Handshake timestamp for {client} is 0"
|
||
|
|
age = now - ts
|
||
|
|
assert age < 300, f"Handshake for {client} is {age:.0f}s old (stale?)"
|
||
|
|
|
||
|
|
|
||
|
|
def test_connected_status_per_client(wait_for_metrics):
|
||
|
|
"""Each client reports wiregui_peer_connected = 1."""
|
||
|
|
for client in EXPECTED_CLIENTS:
|
||
|
|
result = _vm_query(f'wiregui_peer_connected{{device_name="{client}"}}')
|
||
|
|
data = result["data"]["result"]
|
||
|
|
assert len(data) > 0, f"No connected status for {client}"
|
||
|
|
value = int(float(data[0]["value"][1]))
|
||
|
|
assert value == 1, f"Client {client} not connected (wiregui_peer_connected={value})"
|
||
|
|
|
||
|
|
|
||
|
|
def test_db_devices_have_stats():
|
||
|
|
"""Verify device rows in PostgreSQL also have updated stats."""
|
||
|
|
import asyncio
|
||
|
|
from sqlmodel import select
|
||
|
|
from wiregui.db import async_session, engine
|
||
|
|
from wiregui.models.device import Device
|
||
|
|
|
||
|
|
async def check():
|
||
|
|
async with async_session() as session:
|
||
|
|
result = await session.execute(
|
||
|
|
select(Device).where(Device.name.in_(EXPECTED_CLIENTS))
|
||
|
|
)
|
||
|
|
devices = result.scalars().all()
|
||
|
|
|
||
|
|
assert len(devices) == len(EXPECTED_CLIENTS), (
|
||
|
|
f"Expected {len(EXPECTED_CLIENTS)} devices, found {len(devices)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
for device in devices:
|
||
|
|
assert device.latest_handshake is not None, (
|
||
|
|
f"Device {device.name} has no handshake in DB"
|
||
|
|
)
|
||
|
|
assert device.rx_bytes is not None and device.rx_bytes > 0, (
|
||
|
|
f"Device {device.name} has no rx_bytes in DB"
|
||
|
|
)
|
||
|
|
await engine.dispose()
|
||
|
|
|
||
|
|
asyncio.run(check())
|