#!/usr/bin/env python3 """ Unit tests for network/DNS/DHCP 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/dhcp/leases POST /api/dhcp/reservations DELETE /api/dhcp/reservations 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 TestGetDhcpLeases(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.network_manager') def test_get_dhcp_leases_returns_200_with_list(self, mock_nm): mock_nm.get_dhcp_leases.return_value = [ {'mac': 'aa:bb:cc:dd:ee:ff', 'ip': '192.168.1.101', 'hostname': 'laptop'}, ] r = self.client.get('/api/dhcp/leases') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIsInstance(data, list) self.assertEqual(len(data), 1) self.assertEqual(data[0]['hostname'], 'laptop') @patch('app.network_manager') def test_get_dhcp_leases_returns_empty_list_when_no_leases(self, mock_nm): mock_nm.get_dhcp_leases.return_value = [] r = self.client.get('/api/dhcp/leases') self.assertEqual(r.status_code, 200) self.assertEqual(json.loads(r.data), []) @patch('app.network_manager') def test_get_dhcp_leases_returns_500_on_exception(self, mock_nm): mock_nm.get_dhcp_leases.side_effect = Exception('dnsmasq not running') r = self.client.get('/api/dhcp/leases') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddDhcpReservation(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.network_manager') def test_add_reservation_returns_200_on_valid_body(self, mock_nm): mock_nm.add_dhcp_reservation.return_value = True r = self.client.post( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff', 'ip': '192.168.1.50', 'hostname': 'printer'}), 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_reservation_returns_400_when_no_body(self, mock_nm): r = self.client.post('/api/dhcp/reservations') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_nm.add_dhcp_reservation.assert_not_called() @patch('app.network_manager') def test_add_reservation_returns_400_when_mac_missing(self, mock_nm): r = self.client.post( '/api/dhcp/reservations', data=json.dumps({'ip': '192.168.1.50'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.network_manager') def test_add_reservation_returns_400_when_ip_missing(self, mock_nm): r = self.client.post( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.network_manager') def test_add_reservation_uses_empty_hostname_when_omitted(self, mock_nm): mock_nm.add_dhcp_reservation.return_value = True self.client.post( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff', 'ip': '192.168.1.50'}), content_type='application/json', ) mock_nm.add_dhcp_reservation.assert_called_once_with('aa:bb:cc:dd:ee:ff', '192.168.1.50', '') @patch('app.network_manager') def test_add_reservation_returns_500_on_exception(self, mock_nm): mock_nm.add_dhcp_reservation.side_effect = Exception('dnsmasq config error') r = self.client.post( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff', 'ip': '192.168.1.50'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestDeleteDhcpReservation(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.network_manager') def test_delete_reservation_returns_200_on_success(self, mock_nm): mock_nm.remove_dhcp_reservation.return_value = True r = self.client.delete( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff'}), 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_delete_reservation_returns_400_when_mac_missing(self, mock_nm): r = self.client.delete( '/api/dhcp/reservations', data=json.dumps({'hostname': 'printer'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_nm.remove_dhcp_reservation.assert_not_called() @patch('app.network_manager') def test_delete_reservation_returns_400_when_no_body(self, mock_nm): r = self.client.delete('/api/dhcp/reservations') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.network_manager') def test_delete_reservation_returns_500_on_exception(self, mock_nm): mock_nm.remove_dhcp_reservation.side_effect = Exception('reservation not found') r = self.client.delete( '/api/dhcp/reservations', data=json.dumps({'mac': 'aa:bb:cc:dd:ee:ff'}), content_type='application/json', ) 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()