#!/usr/bin/env python3 """ Unit tests for routing Flask endpoints in api/app.py. Covers: POST /api/routing/peers (peer_name + peer_ip required) POST /api/routing/exit-nodes (peer_name + peer_ip required) POST /api/routing/bridge (source_peer + target_peer required) POST /api/routing/split (network + exit_peer required) GET /api/routing/peers DELETE /api/routing/peers/ """ 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 TestAddPeerRoute(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_add_peer_route_returns_200_on_success(self, mock_rm): mock_rm.add_peer_route.return_value = True r = self.client.post( '/api/routing/peers', data=json.dumps({'peer_name': 'alice', 'peer_ip': '10.0.0.2'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('added', data) @patch('app.routing_manager') def test_add_peer_route_returns_400_when_peer_name_missing(self, mock_rm): r = self.client.post( '/api/routing/peers', data=json.dumps({'peer_ip': '10.0.0.2'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_peer_route.assert_not_called() @patch('app.routing_manager') def test_add_peer_route_returns_400_when_peer_ip_missing(self, mock_rm): r = self.client.post( '/api/routing/peers', data=json.dumps({'peer_name': 'alice'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_peer_route.assert_not_called() @patch('app.routing_manager') def test_add_peer_route_returns_500_on_exception(self, mock_rm): mock_rm.add_peer_route.side_effect = Exception('iptables error') r = self.client.post( '/api/routing/peers', data=json.dumps({'peer_name': 'alice', 'peer_ip': '10.0.0.2'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddExitNode(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_add_exit_node_returns_200_on_success(self, mock_rm): mock_rm.add_exit_node.return_value = True r = self.client.post( '/api/routing/exit-nodes', data=json.dumps({'peer_name': 'gw', 'peer_ip': '10.0.0.5'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('added', data) @patch('app.routing_manager') def test_add_exit_node_returns_400_when_peer_name_missing(self, mock_rm): r = self.client.post( '/api/routing/exit-nodes', data=json.dumps({'peer_ip': '10.0.0.5'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_exit_node.assert_not_called() @patch('app.routing_manager') def test_add_exit_node_returns_400_when_peer_ip_missing(self, mock_rm): r = self.client.post( '/api/routing/exit-nodes', data=json.dumps({'peer_name': 'gw'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_exit_node.assert_not_called() @patch('app.routing_manager') def test_add_exit_node_returns_500_on_exception(self, mock_rm): mock_rm.add_exit_node.side_effect = Exception('routing table full') r = self.client.post( '/api/routing/exit-nodes', data=json.dumps({'peer_name': 'gw', 'peer_ip': '10.0.0.5'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddBridgeRoute(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_add_bridge_returns_200_on_success(self, mock_rm): mock_rm.add_bridge_route.return_value = True r = self.client.post( '/api/routing/bridge', data=json.dumps({'source_peer': 'alice', 'target_peer': 'bob'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('added', data) @patch('app.routing_manager') def test_add_bridge_returns_400_when_source_peer_missing(self, mock_rm): r = self.client.post( '/api/routing/bridge', data=json.dumps({'target_peer': 'bob'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_bridge_route.assert_not_called() @patch('app.routing_manager') def test_add_bridge_returns_400_when_target_peer_missing(self, mock_rm): r = self.client.post( '/api/routing/bridge', data=json.dumps({'source_peer': 'alice'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_bridge_route.assert_not_called() @patch('app.routing_manager') def test_add_bridge_returns_500_on_exception(self, mock_rm): mock_rm.add_bridge_route.side_effect = Exception('bridge setup failed') r = self.client.post( '/api/routing/bridge', data=json.dumps({'source_peer': 'alice', 'target_peer': 'bob'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddSplitRoute(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_add_split_returns_200_on_success(self, mock_rm): mock_rm.add_split_route.return_value = True r = self.client.post( '/api/routing/split', data=json.dumps({'network': '192.168.10.0/24', 'exit_peer': 'gw'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('added', data) @patch('app.routing_manager') def test_add_split_returns_400_when_network_missing(self, mock_rm): r = self.client.post( '/api/routing/split', data=json.dumps({'exit_peer': 'gw'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_split_route.assert_not_called() @patch('app.routing_manager') def test_add_split_returns_400_when_exit_peer_missing(self, mock_rm): r = self.client.post( '/api/routing/split', data=json.dumps({'network': '192.168.10.0/24'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_rm.add_split_route.assert_not_called() @patch('app.routing_manager') def test_add_split_returns_500_on_exception(self, mock_rm): mock_rm.add_split_route.side_effect = Exception('split tunnel error') r = self.client.post( '/api/routing/split', data=json.dumps({'network': '192.168.10.0/24', 'exit_peer': 'gw'}), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestGetPeerRoutes(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_get_peer_routes_returns_200_with_routes(self, mock_rm): mock_rm.get_peer_routes.return_value = [ {'peer_name': 'alice', 'peer_ip': '10.0.0.2', 'route_type': 'lan'}, ] r = self.client.get('/api/routing/peers') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('peer_routes', data) self.assertIsInstance(data['peer_routes'], list) @patch('app.routing_manager') def test_get_peer_routes_returns_empty_list_when_no_routes(self, mock_rm): mock_rm.get_peer_routes.return_value = [] r = self.client.get('/api/routing/peers') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertEqual(data['peer_routes'], []) @patch('app.routing_manager') def test_get_peer_routes_returns_500_on_exception(self, mock_rm): mock_rm.get_peer_routes.side_effect = Exception('DB error') r = self.client.get('/api/routing/peers') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestDeletePeerRoute(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') def test_delete_peer_route_returns_200_on_success(self, mock_rm): mock_rm.remove_peer_route.return_value = {'removed': True} r = self.client.delete('/api/routing/peers/alice') self.assertEqual(r.status_code, 200) @patch('app.routing_manager') def test_delete_peer_route_calls_manager_with_name(self, mock_rm): mock_rm.remove_peer_route.return_value = {'removed': True} self.client.delete('/api/routing/peers/bob') mock_rm.remove_peer_route.assert_called_once_with('bob') @patch('app.routing_manager') def test_delete_peer_route_returns_500_on_exception(self, mock_rm): mock_rm.remove_peer_route.side_effect = Exception('iptables flush error') r = self.client.delete('/api/routing/peers/alice') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) if __name__ == '__main__': unittest.main()