Files
gandalf/tests/test_app.py
T
jaredandClaude Opus 5.5 e32499b7f6
Lint / Python (flake8) (pull_request) Successful in 2m6s
Lint / JS (eslint) (pull_request) Successful in 18s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 2m11s
Test / Python Tests (pytest) (pull_request) Successful in 2m20s
Lint / JS (eslint) (push) Successful in 10s
Lint / Python (flake8) (push) Successful in 1m58s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m58s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 58s
Add AT&T modem monitoring and device pages; replace Link Debug
Modem and WAN:
- Poll the BGW320 status pages read-only for light levels, PON and WAN
  state, with a WAN / Fiber panel, 7-day Rx trend, and alerts on WAN down,
  optical limits, and Rx drift
- Read WAN outages and Smart Power restarts from the UniFi system log,
  since outages can be over between 5 minute polls

Device pages:
- Add device search to the dashboard and a page per host, switch, router,
  AP, PDU, or modem
- Move all Link Debug data into expandable rows there; /links redirects
- Collect UDM ports and UniFi device details (firmware, load, WAN, outlets)
- Reorder the dashboard

Monitoring fixes:
- Alert when a configured host stops reporting: its series disappear
  rather than reporting down, so a dead host never raised an alert
- Alert on Prometheus outages instead of blanking the dashboard
- Treat a failed Pulse ping as unknown rather than host unreachable
- Stop new switch ports reporting lifetime bytes as one interval's rate
- Run database sessions in UTC: other server time zones shifted displayed
  ages and timed suppression expiries

Also log every avatar 404 reason, add a favicon, and correct the WAN link
labels (1G on Port 9, not 10G SFP+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:40:17 -05:00

94 lines
3.7 KiB
Python

"""Tests for app.py routes: database and LDAP are mocked."""
import json
import os
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import app as gandalf # noqa: E402
AUTH = {'Remote-User': 'jared', 'Remote-Name': 'Jared V', 'Remote-Groups': 'admin'}
STATE = {
'network_snapshot': json.dumps({
'hosts': {'large1': {'ip': '10.0.0.2', 'status': 'up', 'source': 'prometheus', 'interfaces': {'eth0': 'up'}}},
'unifi': [{'name': 'USW Flex', 'type': 'usw', 'model': 'USF5P', 'ip': '10.0.0.3', 'mac': '',
'connected': True}],
}),
'link_stats': json.dumps({'hosts': {}, 'unifi_switches': {}}),
}
@pytest.fixture
def client():
with patch.object(gandalf, 'db') as db:
db.get_state.side_effect = lambda key, default=None: STATE.get(key, default)
db.get_active_events.return_value = []
db.get_recent_resolved.return_value = []
db.get_active_suppressions.return_value = []
db.get_events_for_target.return_value = []
db.count_active_events.return_value = 0
db.get_status_summary.return_value = {'critical': 0, 'warning': 0, 'info': 0, 'resolved_24h': 0}
db.check_suppressed.return_value = False
with patch.object(gandalf, '_cfg', {'auth': {'allowed_groups': ['admin']}, 'ticket_api': {}}):
yield gandalf.app.test_client()
class TestPagesAndNav:
def test_home_has_search_and_no_link_debug(self, client):
html = client.get('/', headers=AUTH).get_data(as_text=True)
assert 'id="device-search-input"' in html
assert 'Link Debug' not in html
def test_home_section_order(self, client):
html = client.get('/', headers=AUTH).get_data(as_text=True)
order = [html.index(m) for m in (
'id="device-search"', 'Alert Queue', 'WAN / Fiber', 'g-section-title">Hosts<',
'Device Inventory', 'Network Topology',
)]
assert order == sorted(order)
def test_old_links_url_redirects_home(self, client):
resp = client.get('/links', headers=AUTH)
assert resp.status_code == 302 and resp.headers['Location'].endswith('/')
def test_device_page_escapes_name(self, client):
html = client.get('/device/%3Cscript%3Ex', headers=AUTH).get_data(as_text=True)
assert '<script>x' not in html
def test_link_data_api_remains_for_inspector(self, client):
assert client.get('/api/links', headers=AUTH).status_code == 200
class TestDeviceApi:
def test_requires_auth(self, client):
assert client.get('/api/devices').status_code == 401
assert client.get('/api/device/large1').status_code == 401
def test_index_lists_devices(self, client):
names = [d['name'] for d in client.get('/api/devices', headers=AUTH).get_json()['devices']]
assert names == ['large1', 'USW Flex']
def test_name_with_a_space(self, client):
d = client.get('/api/device/USW%20Flex', headers=AUTH).get_json()
assert d['kind'] == 'switch' and d['events'] == []
def test_unknown_device_is_404(self, client):
assert client.get('/api/device/nope', headers=AUTH).status_code == 404
class TestAvatarLogging:
def test_missing_ldap_config_says_why(self, client, caplog):
assert client.get('/api/avatar', headers=AUTH).status_code == 404
assert 'no ldap' in caplog.text
class TestDatabaseTimeZone:
def test_sessions_run_in_utc(self):
import db as real_db
with patch.object(real_db.pymysql, 'connect') as connect:
real_db._new_conn({'host': 'h', 'user': 'u', 'password': 'p', 'name': 'n'})
assert connect.call_args.kwargs['init_command'] == "SET time_zone='+00:00'"