420dced9ff
- api/app.py: sync WireGuard server config on peer add/remove (non-fatal) - docker-compose.yml: add privileged:true to wireguard service - E2E tests: fix logout selector, DNS IP lookup, wg config DNS line, VIP skip guards, badge text selectors, heading .first, async logout wait - Integration tests: fix 4 tests that sent unauthenticated requests expecting 400 (now use authenticated session helpers); accept 401 as valid in webui proxy test; add password field to service_access validation test - Remove stale tracked config templates (config/api/api/*, config/api/cell.env, etc.) that no longer exist on disk after config layout was reorganised Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
225 lines
8.1 KiB
Python
225 lines
8.1 KiB
Python
"""
|
|
Network services integration tests: DNS records, DHCP leases, DHCP reservations.
|
|
|
|
Note on endpoint shapes discovered from app.py:
|
|
- DELETE /api/dns/records takes a JSON body (not a URL param)
|
|
- DELETE /api/dhcp/reservations takes JSON body with 'mac' field
|
|
- POST /api/dhcp/reservations requires 'mac' and 'ip' fields
|
|
|
|
Run with: pytest tests/integration/test_network_services.py -v
|
|
"""
|
|
import pytest
|
|
import requests
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from conftest import API_BASE, _resolve_admin_pass
|
|
|
|
# Test DNS hostname to use — must be cleaned up after tests
|
|
_TEST_DNS_HOSTNAME = 'inttest-dns-record'
|
|
|
|
_S: requests.Session = None
|
|
|
|
|
|
@pytest.fixture(scope='module', autouse=True)
|
|
def _auth_session():
|
|
global _S
|
|
_S = requests.Session()
|
|
_S.headers['Content-Type'] = 'application/json'
|
|
r = _S.post(f"{API_BASE}/api/auth/login",
|
|
json={'username': 'admin', 'password': _resolve_admin_pass()})
|
|
assert r.status_code == 200, f"Login failed: {r.text}"
|
|
|
|
|
|
def get(path, **kw):
|
|
return _S.get(f"{API_BASE}{path}", **kw)
|
|
|
|
|
|
def post(path, **kw):
|
|
return _S.post(f"{API_BASE}{path}", **kw)
|
|
|
|
|
|
def delete(path, **kw):
|
|
return _S.delete(f"{API_BASE}{path}", **kw)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/dns/records
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDnsRecordsRead:
|
|
def test_get_dns_records_returns_200(self):
|
|
r = get('/api/dns/records')
|
|
assert r.status_code == 200
|
|
|
|
def test_get_dns_records_returns_list_or_dict(self):
|
|
# The network_manager may return a list of records or a dict keyed by hostname
|
|
data = get('/api/dns/records').json()
|
|
assert isinstance(data, (list, dict))
|
|
|
|
def test_get_dns_status_returns_200(self):
|
|
r = get('/api/dns/status')
|
|
assert r.status_code == 200
|
|
|
|
def test_get_dns_status_returns_dict(self):
|
|
data = get('/api/dns/status').json()
|
|
assert isinstance(data, dict)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /api/dns/records + DELETE /api/dns/records (round-trip)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDnsRecordsWrite:
|
|
"""Create a DNS A record then delete it. The test is self-cleaning."""
|
|
|
|
def test_add_dns_record_returns_non_error(self):
|
|
"""Adding a well-formed A record should not return a 4xx or 5xx."""
|
|
r = post('/api/dns/records', json={
|
|
'zone': 'cell',
|
|
'name': _TEST_DNS_HOSTNAME,
|
|
'record_type': 'A',
|
|
'value': '10.0.0.99',
|
|
})
|
|
# Accept 200 or 201; clean up regardless
|
|
try:
|
|
assert r.status_code in (200, 201), (
|
|
f"Expected 200/201 for DNS record creation, got {r.status_code}: {r.text}"
|
|
)
|
|
finally:
|
|
delete('/api/dns/records', json={'zone': 'cell', 'name': _TEST_DNS_HOSTNAME, 'record_type': 'A'})
|
|
|
|
def test_add_and_delete_dns_record_round_trip(self):
|
|
"""Create a record, verify it appears in the list, then delete it."""
|
|
add_r = post('/api/dns/records', json={
|
|
'zone': 'cell',
|
|
'name': _TEST_DNS_HOSTNAME,
|
|
'record_type': 'A',
|
|
'value': '10.0.0.98',
|
|
})
|
|
assert add_r.status_code in (200, 201), (
|
|
f"Could not create test DNS record: {add_r.text}"
|
|
)
|
|
try:
|
|
records = get('/api/dns/records').json()
|
|
if isinstance(records, list):
|
|
names = [r.get('name', r.get('hostname', '')) for r in records]
|
|
else:
|
|
names = list(records.keys())
|
|
assert any(_TEST_DNS_HOSTNAME in n for n in names), (
|
|
f"Added record '{_TEST_DNS_HOSTNAME}' not found in records: {records}"
|
|
)
|
|
finally:
|
|
del_r = delete('/api/dns/records', json={'zone': 'cell', 'name': _TEST_DNS_HOSTNAME, 'record_type': 'A'})
|
|
assert del_r.status_code in (200, 204), (
|
|
f"DNS record delete failed: {del_r.status_code} {del_r.text}"
|
|
)
|
|
|
|
def test_delete_nonexistent_dns_record_does_not_crash(self):
|
|
"""Deleting a record that doesn't exist should return 200/404, not 500."""
|
|
r = delete('/api/dns/records', json={'zone': 'cell', 'name': 'does-not-exist-xyz', 'record_type': 'A'})
|
|
assert r.status_code in (200, 404), (
|
|
f"Unexpected status {r.status_code} deleting non-existent DNS record"
|
|
)
|
|
|
|
def test_add_dns_record_missing_name_is_handled(self):
|
|
"""Omitting required fields should not cause an unhandled 500."""
|
|
r = post('/api/dns/records', json={'zone': 'cell', 'record_type': 'A', 'value': '10.0.0.97'})
|
|
assert r.status_code != 200 or 'error' in r.json()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/dhcp/leases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDhcpLeases:
|
|
def test_get_dhcp_leases_returns_200(self):
|
|
r = get('/api/dhcp/leases')
|
|
assert r.status_code == 200
|
|
|
|
def test_get_dhcp_leases_returns_list_or_dict(self):
|
|
data = get('/api/dhcp/leases').json()
|
|
assert isinstance(data, (list, dict))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /api/dhcp/reservations + DELETE /api/dhcp/reservations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_TEST_MAC = 'de:ad:be:ef:11:22'
|
|
_TEST_RESERVATION_IP = '10.0.0.200'
|
|
|
|
|
|
class TestDhcpReservations:
|
|
def _cleanup(self):
|
|
delete('/api/dhcp/reservations', json={'mac': _TEST_MAC})
|
|
|
|
def test_add_dhcp_reservation_returns_non_error(self):
|
|
try:
|
|
r = post('/api/dhcp/reservations', json={
|
|
'mac': _TEST_MAC,
|
|
'ip': _TEST_RESERVATION_IP,
|
|
'hostname': 'inttest-dhcp-host',
|
|
})
|
|
assert r.status_code in (200, 201), (
|
|
f"Expected 200/201 for DHCP reservation, got {r.status_code}: {r.text}"
|
|
)
|
|
finally:
|
|
self._cleanup()
|
|
|
|
def test_add_dhcp_reservation_missing_mac_returns_400(self):
|
|
r = post('/api/dhcp/reservations', json={'ip': _TEST_RESERVATION_IP})
|
|
assert r.status_code == 400
|
|
assert 'error' in r.json()
|
|
|
|
def test_add_dhcp_reservation_missing_ip_returns_400(self):
|
|
r = post('/api/dhcp/reservations', json={'mac': _TEST_MAC})
|
|
assert r.status_code == 400
|
|
assert 'error' in r.json()
|
|
|
|
def test_add_dhcp_reservation_empty_body_returns_400(self):
|
|
r = post('/api/dhcp/reservations', data='')
|
|
assert r.status_code == 400
|
|
|
|
def test_delete_dhcp_reservation_missing_mac_returns_400(self):
|
|
r = delete('/api/dhcp/reservations', json={})
|
|
assert r.status_code == 400
|
|
assert 'error' in r.json()
|
|
|
|
def test_add_and_delete_dhcp_reservation_round_trip(self):
|
|
add_r = post('/api/dhcp/reservations', json={
|
|
'mac': _TEST_MAC,
|
|
'ip': _TEST_RESERVATION_IP,
|
|
})
|
|
assert add_r.status_code in (200, 201), (
|
|
f"Could not create DHCP reservation: {add_r.text}"
|
|
)
|
|
try:
|
|
del_r = delete('/api/dhcp/reservations', json={'mac': _TEST_MAC})
|
|
assert del_r.status_code in (200, 204), (
|
|
f"DHCP reservation delete failed: {del_r.status_code} {del_r.text}"
|
|
)
|
|
except Exception:
|
|
self._cleanup()
|
|
raise
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/ntp/status + GET /api/network/info
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestNtpAndNetworkInfo:
|
|
def test_ntp_status_returns_200(self):
|
|
r = get('/api/ntp/status')
|
|
assert r.status_code == 200
|
|
|
|
def test_ntp_status_is_dict(self):
|
|
assert isinstance(get('/api/ntp/status').json(), dict)
|
|
|
|
def test_network_info_returns_200(self):
|
|
r = get('/api/network/info')
|
|
assert r.status_code == 200
|
|
|
|
def test_network_info_is_dict(self):
|
|
assert isinstance(get('/api/network/info').json(), dict)
|