Files
esp32-web/tests/test_api/test_intelligence.py
user dfbd2a2196 feat: v0.1.4 — device intelligence dashboard
Add tabbed dashboard at /dashboard/ with three D3.js visualizations:
- Vendor treemap (devices grouped by type and vendor)
- SSID social graph (force-directed, shared probed SSIDs as edges)
- Fingerprint clusters (packed circles by device behavior)

Intelligence API endpoints at /api/v1/intelligence/ with param
validation. Dashboard built on htmx + Pico CSS dark theme + D3 v7,
all vendored locally (make vendor). 13 new tests (59 total).
2026-02-06 18:59:53 +01:00

137 lines
4.8 KiB
Python

"""Intelligence API tests."""
from datetime import datetime, UTC, timedelta
from esp32_web.extensions import db
from esp32_web.models import Device, Probe, Sighting, Sensor
def _seed_devices(app):
"""Create test devices, probes, and sightings."""
with app.app_context():
sensor = Sensor(hostname='test-sensor', ip='192.168.1.1')
db.session.add(sensor)
db.session.flush()
d1 = Device(mac='aa:bb:cc:dd:ee:01', device_type='wifi',
vendor='Apple, Inc.', first_seen=datetime.now(UTC),
last_seen=datetime.now(UTC))
d2 = Device(mac='aa:bb:cc:dd:ee:02', device_type='wifi',
vendor='Samsung', first_seen=datetime.now(UTC),
last_seen=datetime.now(UTC))
d3 = Device(mac='aa:bb:cc:dd:ee:03', device_type='ble',
company_id=0x004C, first_seen=datetime.now(UTC),
last_seen=datetime.now(UTC))
db.session.add_all([d1, d2, d3])
db.session.flush()
now = datetime.now(UTC)
# Both d1 and d2 probe "HomeNet"
db.session.add_all([
Probe(device_id=d1.id, sensor_id=sensor.id, ssid='HomeNet',
rssi=-50, channel=6, timestamp=now),
Probe(device_id=d2.id, sensor_id=sensor.id, ssid='HomeNet',
rssi=-60, channel=6, timestamp=now),
Probe(device_id=d1.id, sensor_id=sensor.id, ssid='Office',
rssi=-55, channel=1, timestamp=now),
])
# Sightings
db.session.add_all([
Sighting(device_id=d1.id, sensor_id=sensor.id, rssi=-50, timestamp=now),
Sighting(device_id=d3.id, sensor_id=sensor.id, rssi=-70, timestamp=now),
])
db.session.commit()
# -- Vendor Treemap --
def test_vendor_treemap_empty(client):
"""Test treemap with no devices."""
response = client.get('/api/v1/intelligence/vendor-treemap')
assert response.status_code == 200
assert response.json['name'] == 'devices'
assert response.json['children'] == []
def test_vendor_treemap_with_data(client, app):
"""Test treemap returns grouped vendor data."""
_seed_devices(app)
response = client.get('/api/v1/intelligence/vendor-treemap')
assert response.status_code == 200
data = response.json
assert data['name'] == 'devices'
assert len(data['children']) > 0
# Find wifi group
wifi = next((c for c in data['children'] if c['name'] == 'wifi'), None)
assert wifi is not None
assert len(wifi['children']) >= 1
# Find ble group
ble = next((c for c in data['children'] if c['name'] == 'ble'), None)
assert ble is not None
assert len(ble['children']) >= 1
# -- SSID Graph --
def test_ssid_graph_empty(client):
"""Test graph with no probes."""
response = client.get('/api/v1/intelligence/ssid-graph')
assert response.status_code == 200
assert response.json['nodes'] == []
assert response.json['links'] == []
def test_ssid_graph_with_data(client, app):
"""Test graph returns nodes and links for shared SSIDs."""
_seed_devices(app)
response = client.get('/api/v1/intelligence/ssid-graph?hours=24')
assert response.status_code == 200
data = response.json
assert len(data['nodes']) >= 2
# d1 and d2 share "HomeNet" → at least one link
assert len(data['links']) >= 1
link = data['links'][0]
assert 'HomeNet' in link['shared_ssids']
assert link['weight'] >= 1
# SSID summary present
assert len(data['ssids']) >= 1
def test_ssid_graph_params(client, app):
"""Test graph with custom parameters."""
_seed_devices(app)
response = client.get('/api/v1/intelligence/ssid-graph?hours=1&min_shared=5&limit=10')
assert response.status_code == 200
# With min_shared=5, no pairs should match
assert response.json['links'] == []
# -- Fingerprint Clusters --
def test_fingerprint_clusters_empty(client):
"""Test clusters with no activity."""
response = client.get('/api/v1/intelligence/fingerprint-clusters')
assert response.status_code == 200
assert response.json['clusters'] == []
assert response.json['total_devices'] == 0
def test_fingerprint_clusters_with_data(client, app):
"""Test clusters group devices by behavior."""
_seed_devices(app)
response = client.get('/api/v1/intelligence/fingerprint-clusters?hours=24')
assert response.status_code == 200
data = response.json
assert data['total_devices'] >= 2
assert data['total_clusters'] >= 1
# Each cluster has expected structure
cluster = data['clusters'][0]
assert 'label' in cluster
assert 'device_count' in cluster
assert 'devices' in cluster
assert 'centroid' in cluster
assert 'probe_rate' in cluster['centroid']
assert 'sighting_rate' in cluster['centroid']