#!/usr/bin/env python3 """ Unit tests for cell management Flask endpoints in api/app.py. Covers: GET /api/cells/invite — generate invite package GET /api/cells — list connected cells POST /api/cells — connect to a remote cell DELETE /api/cells/ — disconnect from a cell GET /api/cells//status — live status for a connected cell """ 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 # Minimal set of required fields for POST /api/cells _VALID_CELL_BODY = { 'cell_name': 'remotecell', 'public_key': 'abc123publickey==', 'vpn_subnet': '10.1.0.0/24', 'dns_ip': '10.1.0.1', 'domain': 'remotecell.cell', } class TestGetCellInvite(unittest.TestCase): """GET /api/cells/invite""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.cell_link_manager') @patch('app.config_manager') def test_get_invite_returns_200_with_invite_dict(self, mock_cfg, mock_clm): mock_cfg.configs = {'_identity': {'cell_name': 'mycell', 'domain': 'cell'}} mock_clm.generate_invite.return_value = { 'cell_name': 'mycell', 'public_key': 'server_pub_key==', 'vpn_subnet': '10.0.0.0/24', 'dns_ip': '10.0.0.1', 'domain': 'cell', } r = self.client.get('/api/cells/invite') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('cell_name', data) self.assertIn('public_key', data) @patch('app.cell_link_manager') @patch('app.config_manager') def test_get_invite_passes_cell_name_and_domain(self, mock_cfg, mock_clm): mock_cfg.configs = {'_identity': {'cell_name': 'myhome', 'domain': 'home'}} mock_clm.generate_invite.return_value = {} self.client.get('/api/cells/invite') mock_clm.generate_invite.assert_called_once_with('myhome', 'home') @patch('app.cell_link_manager') @patch('app.config_manager') def test_get_invite_returns_500_on_exception(self, mock_cfg, mock_clm): mock_cfg.configs = {'_identity': {}} mock_clm.generate_invite.side_effect = Exception('WireGuard key unavailable') r = self.client.get('/api/cells/invite') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestListCellConnections(unittest.TestCase): """GET /api/cells""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.cell_link_manager') def test_list_cells_returns_200_with_list(self, mock_clm): mock_clm.list_connections.return_value = [ {'cell_name': 'remotecell', 'domain': 'remotecell.cell', 'status': 'connected'}, ] r = self.client.get('/api/cells') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIsInstance(data, list) self.assertEqual(len(data), 1) self.assertEqual(data[0]['cell_name'], 'remotecell') @patch('app.cell_link_manager') def test_list_cells_returns_empty_list_when_none_connected(self, mock_clm): mock_clm.list_connections.return_value = [] r = self.client.get('/api/cells') self.assertEqual(r.status_code, 200) self.assertEqual(json.loads(r.data), []) @patch('app.cell_link_manager') def test_list_cells_returns_500_on_exception(self, mock_clm): mock_clm.list_connections.side_effect = Exception('storage error') r = self.client.get('/api/cells') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestAddCellConnection(unittest.TestCase): """POST /api/cells""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.cell_link_manager') def test_add_cell_returns_201_on_success(self, mock_clm): mock_clm.add_connection.return_value = {'cell_name': 'remotecell'} r = self.client.post( '/api/cells', data=json.dumps(_VALID_CELL_BODY), content_type='application/json', ) self.assertEqual(r.status_code, 201) data = json.loads(r.data) self.assertIn('message', data) self.assertIn('link', data) @patch('app.cell_link_manager') def test_add_cell_returns_400_when_no_body(self, mock_clm): r = self.client.post('/api/cells') self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) mock_clm.add_connection.assert_not_called() @patch('app.cell_link_manager') def test_add_cell_returns_400_when_cell_name_missing(self, mock_clm): body = {k: v for k, v in _VALID_CELL_BODY.items() if k != 'cell_name'} r = self.client.post( '/api/cells', data=json.dumps(body), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_400_when_public_key_missing(self, mock_clm): body = {k: v for k, v in _VALID_CELL_BODY.items() if k != 'public_key'} r = self.client.post( '/api/cells', data=json.dumps(body), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_400_when_vpn_subnet_missing(self, mock_clm): body = {k: v for k, v in _VALID_CELL_BODY.items() if k != 'vpn_subnet'} r = self.client.post( '/api/cells', data=json.dumps(body), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_400_when_dns_ip_missing(self, mock_clm): body = {k: v for k, v in _VALID_CELL_BODY.items() if k != 'dns_ip'} r = self.client.post( '/api/cells', data=json.dumps(body), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_400_when_domain_missing(self, mock_clm): body = {k: v for k, v in _VALID_CELL_BODY.items() if k != 'domain'} r = self.client.post( '/api/cells', data=json.dumps(body), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_400_on_value_error_from_manager(self, mock_clm): mock_clm.add_connection.side_effect = ValueError('cell already connected') r = self.client.post( '/api/cells', data=json.dumps(_VALID_CELL_BODY), content_type='application/json', ) self.assertEqual(r.status_code, 400) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_add_cell_returns_500_on_unexpected_exception(self, mock_clm): mock_clm.add_connection.side_effect = Exception('WireGuard peer add failed') r = self.client.post( '/api/cells', data=json.dumps(_VALID_CELL_BODY), content_type='application/json', ) self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestRemoveCellConnection(unittest.TestCase): """DELETE /api/cells/""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.cell_link_manager') def test_remove_cell_returns_200_on_success(self, mock_clm): mock_clm.remove_connection.return_value = None r = self.client.delete('/api/cells/remotecell') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('message', data) @patch('app.cell_link_manager') def test_remove_cell_passes_cell_name_to_manager(self, mock_clm): mock_clm.remove_connection.return_value = None self.client.delete('/api/cells/faraway') mock_clm.remove_connection.assert_called_once_with('faraway') @patch('app.cell_link_manager') def test_remove_cell_returns_404_on_value_error(self, mock_clm): mock_clm.remove_connection.side_effect = ValueError('cell not found') r = self.client.delete('/api/cells/nonexistent') self.assertEqual(r.status_code, 404) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_remove_cell_returns_500_on_unexpected_exception(self, mock_clm): mock_clm.remove_connection.side_effect = Exception('storage corruption') r = self.client.delete('/api/cells/remotecell') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) class TestGetCellConnectionStatus(unittest.TestCase): """GET /api/cells//status""" def setUp(self): app.config['TESTING'] = True self.client = app.test_client() @patch('app.cell_link_manager') def test_get_cell_status_returns_200_with_status_dict(self, mock_clm): mock_clm.get_connection_status.return_value = { 'cell_name': 'remotecell', 'online': True, 'last_handshake': '2026-04-27T09:00:00Z', 'transfer_rx': 1024, 'transfer_tx': 2048, } r = self.client.get('/api/cells/remotecell/status') self.assertEqual(r.status_code, 200) data = json.loads(r.data) self.assertIn('online', data) self.assertTrue(data['online']) @patch('app.cell_link_manager') def test_get_cell_status_passes_cell_name(self, mock_clm): mock_clm.get_connection_status.return_value = {} self.client.get('/api/cells/faraway/status') mock_clm.get_connection_status.assert_called_once_with('faraway') @patch('app.cell_link_manager') def test_get_cell_status_returns_404_on_value_error(self, mock_clm): mock_clm.get_connection_status.side_effect = ValueError('cell not found') r = self.client.get('/api/cells/missing/status') self.assertEqual(r.status_code, 404) self.assertIn('error', json.loads(r.data)) @patch('app.cell_link_manager') def test_get_cell_status_returns_500_on_unexpected_exception(self, mock_clm): mock_clm.get_connection_status.side_effect = Exception('WireGuard query failed') r = self.client.get('/api/cells/remotecell/status') self.assertEqual(r.status_code, 500) self.assertIn('error', json.loads(r.data)) if __name__ == '__main__': unittest.main()