Files
pic/tests/test_network_endpoints.py
T
roof aa1e5c41ec
Unit Tests / test (push) Successful in 12m6s
test: raise coverage 68.7% -> ~80.4%; add ~250 tests for new egress/DDNS/network paths
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>
2026-06-10 09:03:39 -04:00

243 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""
Unit tests for network/DNS Flask endpoints in api/app.py.
Covers:
GET /api/dns/records
POST /api/dns/records
DELETE /api/dns/records
GET /api/dns/status
GET /api/dns/overview
GET /api/network/info
POST /api/network/test
"""
import sys
import json
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock
api_dir = Path(__file__).parent.parent / 'api'
sys.path.insert(0, str(api_dir))
from app import app
class TestGetDnsRecords(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_get_dns_records_returns_200_with_list(self, mock_nm):
mock_nm.get_dns_records.return_value = [
{'name': 'myhost.cell', 'type': 'A', 'value': '192.168.1.10'},
{'name': 'nas.cell', 'type': 'A', 'value': '192.168.1.20'},
]
r = self.client.get('/api/dns/records')
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertIsInstance(data, list)
self.assertEqual(len(data), 2)
@patch('app.network_manager')
def test_get_dns_records_returns_empty_list_when_none(self, mock_nm):
mock_nm.get_dns_records.return_value = []
r = self.client.get('/api/dns/records')
self.assertEqual(r.status_code, 200)
self.assertEqual(json.loads(r.data), [])
@patch('app.network_manager')
def test_get_dns_records_returns_500_on_exception(self, mock_nm):
mock_nm.get_dns_records.side_effect = Exception('CoreDNS unreachable')
r = self.client.get('/api/dns/records')
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestAddDnsRecord(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_add_dns_record_returns_200_on_valid_body(self, mock_nm):
mock_nm.add_dns_record.return_value = {'success': True}
r = self.client.post(
'/api/dns/records',
data=json.dumps({'name': 'printer.cell', 'type': 'A', 'value': '192.168.1.50'}),
content_type='application/json',
)
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertIn('success', data)
@patch('app.network_manager')
def test_add_dns_record_returns_400_when_no_body(self, mock_nm):
r = self.client.post('/api/dns/records')
self.assertEqual(r.status_code, 400)
self.assertIn('error', json.loads(r.data))
mock_nm.add_dns_record.assert_not_called()
@patch('app.network_manager')
def test_add_dns_record_returns_500_on_exception(self, mock_nm):
mock_nm.add_dns_record.side_effect = Exception('Corefile write failed')
r = self.client.post(
'/api/dns/records',
data=json.dumps({'name': 'bad.cell', 'type': 'A', 'value': '10.0.0.1'}),
content_type='application/json',
)
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestDeleteDnsRecord(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_delete_dns_record_returns_200_on_success(self, mock_nm):
mock_nm.remove_dns_record.return_value = {'success': True}
r = self.client.delete(
'/api/dns/records',
data=json.dumps({'name': 'printer.cell'}),
content_type='application/json',
)
self.assertEqual(r.status_code, 200)
@patch('app.network_manager')
def test_delete_dns_record_returns_500_on_exception(self, mock_nm):
mock_nm.remove_dns_record.side_effect = Exception('record not found')
r = self.client.delete(
'/api/dns/records',
data=json.dumps({'name': 'missing.cell'}),
content_type='application/json',
)
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestGetDnsStatus(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_get_dns_status_returns_200_with_status_dict(self, mock_nm):
mock_nm.get_dns_status.return_value = {
'running': True,
'records_count': 5,
'upstreams': ['1.1.1.1', '8.8.8.8'],
}
r = self.client.get('/api/dns/status')
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertIn('running', data)
@patch('app.network_manager')
def test_get_dns_status_returns_500_on_exception(self, mock_nm):
mock_nm.get_dns_status.side_effect = Exception('CoreDNS not running')
r = self.client.get('/api/dns/status')
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestGetDnsOverview(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.ddns_manager')
@patch('app.config_manager')
@patch('app.network_manager')
def test_get_dns_overview_returns_200(self, mock_nm, mock_cm, mock_dm):
mock_nm.get_dns_overview.return_value = {
'mode': 'pic_ngo',
'provider': 'pic_ngo',
'effective_domain': 'mycell.pic.ngo',
'internal_domain': 'cell',
'public_ip': '1.2.3.4',
'public_records': [],
'internal_records': [],
'service_subdomains': [],
'registration_status': {'registered': True},
}
r = self.client.get('/api/dns/overview')
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertEqual(data['mode'], 'pic_ngo')
self.assertEqual(data['effective_domain'], 'mycell.pic.ngo')
mock_nm.get_dns_overview.assert_called_once_with(mock_cm, mock_dm)
@patch('app.ddns_manager')
@patch('app.config_manager')
@patch('app.network_manager')
def test_get_dns_overview_returns_500_on_exception(self, mock_nm, mock_cm, mock_dm):
mock_nm.get_dns_overview.side_effect = Exception('boom')
r = self.client.get('/api/dns/overview')
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestGetNetworkInfo(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_get_network_info_returns_200_with_info_dict(self, mock_nm):
mock_nm.get_network_info.return_value = {
'interfaces': ['eth0', 'wg0'],
'gateway': '192.168.1.1',
'dns': ['127.0.0.1'],
}
r = self.client.get('/api/network/info')
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertIn('interfaces', data)
@patch('app.network_manager')
def test_get_network_info_returns_500_on_exception(self, mock_nm):
mock_nm.get_network_info.side_effect = Exception('network unreachable')
r = self.client.get('/api/network/info')
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
class TestNetworkTest(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
@patch('app.network_manager')
def test_network_test_returns_200_with_result(self, mock_nm):
mock_nm.test_connectivity.return_value = {
'internet': True,
'dns': True,
'latency_ms': 15,
}
r = self.client.post('/api/network/test')
self.assertEqual(r.status_code, 200)
data = json.loads(r.data)
self.assertIn('internet', data)
@patch('app.network_manager')
def test_network_test_returns_500_on_exception(self, mock_nm):
mock_nm.test_connectivity.side_effect = Exception('ping failed')
r = self.client.post('/api/network/test')
self.assertEqual(r.status_code, 500)
self.assertIn('error', json.loads(r.data))
if __name__ == '__main__':
unittest.main()