09138fbc18
Extract 9 route groups out of app.py into routes/ blueprints:
- routes/network.py — DNS, DHCP, NTP, network info/test (10 routes)
- routes/wireguard.py — WireGuard keys, peers, config, enforcement (18 routes)
- routes/cells.py — cell-to-cell connections (5 routes)
- routes/peers.py — peer CRUD + IP update + _next_peer_ip helper (10 routes)
- routes/routing.py — NAT, peer routes, firewall, iptables (17 routes)
- routes/vault.py — certs, trust, secrets (19 routes)
- routes/containers.py — containers, images, volumes (14 routes)
- routes/services.py — service bus, logs, services status/connectivity (18 routes)
- routes/peer_dashboard.py — peer-scoped dashboard/services (2 routes)
All blueprints use lazy `from app import X` inside route bodies to preserve
test patch compatibility (patch('app.email_manager', mock) still works).
Also included in this commit:
- A1 fix: backup/restore now includes email/calendar user files
- A2 fix: apply_config sets applying=True flag via helper container
- A3 fix: add_peer rolls back firewall on DNS failure
app.py reduced: 3011 → 1294 lines. 1021 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
6.2 KiB
Python
183 lines
6.2 KiB
Python
#!/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/<name>/clear-reinstall: success (200)
|
|
- POST /api/peers/<name>/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('routes.peers._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('routes.peers._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/<name>/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()
|