#!/usr/bin/env python3 """ Edge-case tests for peer management endpoints in api/app.py. Key scenarios: - POST /api/peers with subnet exhaustion (_next_peer_ip raises ValueError) → 409 - POST /api/peers//clear-reinstall: success (200) - POST /api/peers//clear-reinstall: unknown peer raises → 500 - POST /api/ip-update: missing 'peer' field → 400 - POST /api/ip-update: missing 'ip' field → 400 - POST /api/ip-update: unknown peer → 404 - POST /api/ip-update: success → 200 """ 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 TestAddPeerSubnetExhaustion(unittest.TestCase): """POST /api/peers with no free IPs left must return 409, not 500.""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app._next_peer_ip') @patch('app.auth_manager') def test_add_peer_returns_409_when_subnet_exhausted(self, mock_auth, mock_next_ip): mock_auth.create_user.return_value = True mock_next_ip.side_effect = ValueError('No free IPs left in 10.0.0.0/24') r = self.client.post( '/api/peers', data=json.dumps({ 'name': 'newpeer', 'public_key': 'PUBKEY==', 'password': 'verysecret123', }), content_type='application/json', ) self.assertEqual(r.status_code, 409) data = json.loads(r.data) self.assertIn('error', data) @patch('app._next_peer_ip') @patch('app.auth_manager') def test_add_peer_409_error_message_mentions_ip(self, mock_auth, mock_next_ip): mock_auth.create_user.return_value = True mock_next_ip.side_effect = ValueError('No free IPs left in 10.0.0.0/24') r = self.client.post( '/api/peers', data=json.dumps({ 'name': 'newpeer', 'public_key': 'PUBKEY==', 'password': 'verysecret123', }), content_type='application/json', ) self.assertEqual(r.status_code, 409) data = json.loads(r.data) self.assertIn('No free IPs', data['error']) class TestClearReinstallFlag(unittest.TestCase): """POST /api/peers//clear-reinstall""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.peer_registry') def test_clear_reinstall_returns_200_on_success(self, mock_reg): mock_reg.clear_reinstall_flag.return_value = True r = self.client.post('/api/peers/alice/clear-reinstall') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('message', data) @patch('app.peer_registry') def test_clear_reinstall_calls_registry_with_peer_name(self, mock_reg): mock_reg.clear_reinstall_flag.return_value = True self.client.post('/api/peers/bob/clear-reinstall') mock_reg.clear_reinstall_flag.assert_called_once_with('bob') @patch('app.peer_registry') def test_clear_reinstall_returns_500_when_exception_raised(self, mock_reg): mock_reg.clear_reinstall_flag.side_effect = Exception('peer not found') r = self.client.post('/api/peers/ghost/clear-reinstall') self.assertEqual(r.status_code, 500) data = json.loads(r.data) self.assertIn('error', data) class TestIpUpdate(unittest.TestCase): """POST /api/ip-update""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.routing_manager') @patch('app.peer_registry') def test_ip_update_returns_200_on_success(self, mock_reg, mock_rm): mock_reg.update_peer_ip.return_value = True mock_rm.update_peer_ip.return_value = None r = self.client.post( '/api/ip-update', data=json.dumps({'peer': 'alice', 'ip': '10.0.0.99'}), content_type='application/json', ) self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('message', data) @patch('app.peer_registry') def test_ip_update_returns_400_when_peer_field_missing(self, mock_reg): r = self.client.post( '/api/ip-update', data=json.dumps({'ip': '10.0.0.99'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) data = json.loads(r.data) self.assertIn('error', data) mock_reg.update_peer_ip.assert_not_called() @patch('app.peer_registry') def test_ip_update_returns_400_when_ip_field_missing(self, mock_reg): r = self.client.post( '/api/ip-update', data=json.dumps({'peer': 'alice'}), content_type='application/json', ) self.assertEqual(r.status_code, 400) data = json.loads(r.data) self.assertIn('error', data) mock_reg.update_peer_ip.assert_not_called() @patch('app.peer_registry') def test_ip_update_returns_400_when_no_body(self, mock_reg): r = self.client.post('/api/ip-update') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.peer_registry') def test_ip_update_returns_404_when_peer_not_found(self, mock_reg): mock_reg.update_peer_ip.return_value = False r = self.client.post( '/api/ip-update', data=json.dumps({'peer': 'ghost', 'ip': '10.0.0.50'}), content_type='application/json', ) self.assertEqual(r.status_code, 404) data = json.loads(r.data) self.assertIn('error', data) @patch('app.routing_manager') @patch('app.peer_registry') def test_ip_update_calls_registry_with_correct_args(self, mock_reg, mock_rm): mock_reg.update_peer_ip.return_value = True mock_rm.update_peer_ip.return_value = None self.client.post( '/api/ip-update', data=json.dumps({'peer': 'alice', 'ip': '10.0.0.5'}), content_type='application/json', ) mock_reg.update_peer_ip.assert_called_once_with('alice', '10.0.0.5') if __name__ == '__main__': unittest.main()