aa1e5c41ec
Unit Tests / test (push) Successful in 12m6s
Coverage was below acceptable levels and several newly-added code paths (sshuttle egress, proxy egress, DDNS provider stubs, DNS overview route, peer-registry provisioning) had zero test coverage. ~250 new unit tests are added across 16 new test files. Existing test files are updated to match refactored interfaces (DHCP removed, constants introduced, network_manager restructured). .coveragerc is added to pin the source mapping and the 70% floor so regressions are caught at commit time. tests/test_enhanced_api.py was previously living in api/ (wrong location) and is moved to tests/ where it belongs. Integration test files are updated to remove references to DHCP endpoints and add coverage for the new DNS overview and DDNS sync endpoints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
"""
|
|
Network services integration tests: DNS records, DNS overview.
|
|
|
|
Note on endpoint shapes discovered from app.py:
|
|
- DELETE /api/dns/records takes a JSON body (not a URL param)
|
|
|
|
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/dns/overview
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDnsOverview:
|
|
def test_get_dns_overview_returns_200(self):
|
|
r = get('/api/dns/overview')
|
|
assert r.status_code == 200
|
|
|
|
def test_get_dns_overview_has_expected_keys(self):
|
|
data = get('/api/dns/overview').json()
|
|
assert isinstance(data, dict)
|
|
for key in ('mode', 'effective_domain', 'internal_domain',
|
|
'public_records', 'internal_records'):
|
|
assert key in data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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)
|