feat(cells): Phase 3 tests + Phase 4 UI for cell service-sharing
Phase 3 — tests (50 new, total now 1071): - test_cell_link_manager: atomicity (WG fail → DNS not called, link not persisted), DNS warning non-fatal, inbound_services arg, unknown service filtered, update/get permissions, lazy migration of legacy entries - test_wireguard_manager: subnet overlap rejection (exact, supernet, adjacent non-overlapping, different class-A, honours wg0.conf configured network) - test_firewall_manager: _cell_tag sanitisation, apply_cell_rules emits correct ACCEPT/DROP per service + catch-all DROP, clear_cell_rules no-op and exact line removal, apply_all_cell_rules iterates with correct args - test_cells_endpoints: RuntimeError→400, GET /services, GET/PUT permissions (200/400/404 paths, service name validation, arg forwarding) Phase 4 — UI: - CellNetwork.jsx: replace flat cell list with CellPanel expandable cards; add ServiceShareToggle (ARIA switch, saves immediately), InboundServiceBadge (read-only), DisconnectConfirmModal (replaces window.confirm); relative timestamps; paste validation on blur; WireGuard status merged by public_key - api.js: add cellLinkAPI.getPermissions, updatePermissions, getServices Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -291,5 +291,230 @@ class TestGetCellConnectionStatus(unittest.TestCase):
|
||||
self.assertIn('error', json.loads(r.data))
|
||||
|
||||
|
||||
class TestAddCellRuntimeError(unittest.TestCase):
|
||||
"""POST /api/cells — RuntimeError from the manager must now return 400, not 500."""
|
||||
|
||||
def setUp(self):
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_add_cell_runtime_error_returns_400(self, mock_clm):
|
||||
"""When add_connection raises RuntimeError (WG failure), endpoint returns 400."""
|
||||
mock_clm.add_connection.side_effect = RuntimeError('Failed to add WireGuard peer')
|
||||
r = self.client.post(
|
||||
'/api/cells',
|
||||
data=json.dumps(_VALID_CELL_BODY),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('error', data)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_add_cell_runtime_error_body_contains_message(self, mock_clm):
|
||||
"""The 400 response for a RuntimeError includes the error message."""
|
||||
mock_clm.add_connection.side_effect = RuntimeError('WireGuard peer add failed')
|
||||
r = self.client.post(
|
||||
'/api/cells',
|
||||
data=json.dumps(_VALID_CELL_BODY),
|
||||
content_type='application/json',
|
||||
)
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('WireGuard', data['error'])
|
||||
|
||||
|
||||
class TestListServices(unittest.TestCase):
|
||||
"""GET /api/cells/services"""
|
||||
|
||||
def setUp(self):
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
def test_list_services_returns_200(self):
|
||||
"""GET /api/cells/services returns HTTP 200."""
|
||||
r = self.client.get('/api/cells/services')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_list_services_returns_services_key(self):
|
||||
"""Response body has a 'services' key."""
|
||||
r = self.client.get('/api/cells/services')
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('services', data)
|
||||
|
||||
def test_list_services_returns_list(self):
|
||||
"""'services' value is a non-empty list."""
|
||||
r = self.client.get('/api/cells/services')
|
||||
data = json.loads(r.data)
|
||||
self.assertIsInstance(data['services'], list)
|
||||
self.assertGreater(len(data['services']), 0)
|
||||
|
||||
def test_list_services_includes_known_services(self):
|
||||
"""'services' includes the four known shareable services."""
|
||||
r = self.client.get('/api/cells/services')
|
||||
services = json.loads(r.data)['services']
|
||||
for expected in ('calendar', 'files', 'mail', 'webdav'):
|
||||
self.assertIn(expected, services)
|
||||
|
||||
|
||||
class TestGetCellPermissions(unittest.TestCase):
|
||||
"""GET /api/cells/<name>/permissions"""
|
||||
|
||||
def setUp(self):
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_get_permissions_returns_200(self, mock_clm):
|
||||
"""GET /api/cells/office/permissions returns 200 when cell exists."""
|
||||
mock_clm.get_permissions.return_value = {
|
||||
'inbound': {'calendar': True, 'files': False, 'mail': False, 'webdav': False},
|
||||
'outbound': {'calendar': False, 'files': False, 'mail': False, 'webdav': False},
|
||||
}
|
||||
r = self.client.get('/api/cells/office/permissions')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_get_permissions_response_has_inbound_and_outbound(self, mock_clm):
|
||||
"""Response body contains 'inbound' and 'outbound' keys."""
|
||||
mock_clm.get_permissions.return_value = {
|
||||
'inbound': {'calendar': False, 'files': False, 'mail': False, 'webdav': False},
|
||||
'outbound': {'calendar': False, 'files': False, 'mail': False, 'webdav': False},
|
||||
}
|
||||
r = self.client.get('/api/cells/office/permissions')
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('inbound', data)
|
||||
self.assertIn('outbound', data)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_get_permissions_unknown_cell_returns_404(self, mock_clm):
|
||||
"""ValueError from get_permissions maps to 404."""
|
||||
mock_clm.get_permissions.side_effect = ValueError('cell not found')
|
||||
r = self.client.get('/api/cells/nosuchcell/permissions')
|
||||
self.assertEqual(r.status_code, 404)
|
||||
self.assertIn('error', json.loads(r.data))
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_get_permissions_passes_cell_name(self, mock_clm):
|
||||
"""The cell_name URL segment is forwarded to get_permissions."""
|
||||
mock_clm.get_permissions.return_value = {'inbound': {}, 'outbound': {}}
|
||||
self.client.get('/api/cells/faraway/permissions')
|
||||
mock_clm.get_permissions.assert_called_once_with('faraway')
|
||||
|
||||
|
||||
class TestUpdateCellPermissions(unittest.TestCase):
|
||||
"""PUT /api/cells/<name>/permissions"""
|
||||
|
||||
def setUp(self):
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
_VALID_PERM_BODY = {
|
||||
'inbound': {'calendar': True, 'files': False, 'mail': False, 'webdav': False},
|
||||
'outbound': {'calendar': False, 'files': False, 'mail': False, 'webdav': False},
|
||||
}
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
@patch('app.peer_registry')
|
||||
@patch('app.firewall_manager')
|
||||
@patch('app.config_manager')
|
||||
def test_update_permissions_returns_200(self, mock_cfg, mock_fm, mock_pr, mock_clm):
|
||||
"""PUT with valid inbound/outbound returns 200."""
|
||||
mock_cfg.configs = {'_identity': {'domain': 'cell'}}
|
||||
mock_pr.list_peers.return_value = []
|
||||
mock_clm.list_connections.return_value = []
|
||||
mock_clm.update_permissions.return_value = {
|
||||
'cell_name': 'office',
|
||||
'permissions': self._VALID_PERM_BODY,
|
||||
}
|
||||
mock_fm.apply_all_dns_rules.return_value = True
|
||||
r = self.client.put(
|
||||
'/api/cells/office/permissions',
|
||||
data=json.dumps(self._VALID_PERM_BODY),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('message', data)
|
||||
self.assertIn('link', data)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_update_permissions_unknown_service_returns_400(self, mock_clm):
|
||||
"""PUT body containing an unknown service name returns 400."""
|
||||
body = {
|
||||
'inbound': {'bad_service': True, 'calendar': True},
|
||||
'outbound': {},
|
||||
}
|
||||
r = self.client.put(
|
||||
'/api/cells/office/permissions',
|
||||
data=json.dumps(body),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
data = json.loads(r.data)
|
||||
self.assertIn('error', data)
|
||||
# update_permissions should NOT have been called when validation fails
|
||||
mock_clm.update_permissions.assert_not_called()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_update_permissions_unknown_cell_returns_404(self, mock_clm):
|
||||
"""ValueError from update_permissions (cell not found) maps to 404."""
|
||||
mock_clm.update_permissions.side_effect = ValueError('cell not found')
|
||||
r = self.client.put(
|
||||
'/api/cells/nosuchcell/permissions',
|
||||
data=json.dumps(self._VALID_PERM_BODY),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
self.assertIn('error', json.loads(r.data))
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_update_permissions_no_body_returns_400(self, mock_clm):
|
||||
"""PUT with no JSON body returns 400."""
|
||||
r = self.client.put('/api/cells/office/permissions')
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertIn('error', json.loads(r.data))
|
||||
mock_clm.update_permissions.assert_not_called()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_update_permissions_outbound_unknown_service_returns_400(self, mock_clm):
|
||||
"""Unknown service in outbound (not just inbound) also returns 400."""
|
||||
body = {
|
||||
'inbound': {'calendar': True},
|
||||
'outbound': {'hacked': True},
|
||||
}
|
||||
r = self.client.put(
|
||||
'/api/cells/office/permissions',
|
||||
data=json.dumps(body),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
@patch('app.peer_registry')
|
||||
@patch('app.firewall_manager')
|
||||
@patch('app.config_manager')
|
||||
def test_update_permissions_passes_inbound_outbound_to_manager(
|
||||
self, mock_cfg, mock_fm, mock_pr, mock_clm):
|
||||
"""update_permissions is called with inbound and outbound dicts from the body."""
|
||||
mock_cfg.configs = {'_identity': {'domain': 'cell'}}
|
||||
mock_pr.list_peers.return_value = []
|
||||
mock_clm.list_connections.return_value = []
|
||||
mock_clm.update_permissions.return_value = {
|
||||
'cell_name': 'office', 'permissions': self._VALID_PERM_BODY
|
||||
}
|
||||
mock_fm.apply_all_dns_rules.return_value = True
|
||||
self.client.put(
|
||||
'/api/cells/office/permissions',
|
||||
data=json.dumps(self._VALID_PERM_BODY),
|
||||
content_type='application/json',
|
||||
)
|
||||
mock_clm.update_permissions.assert_called_once_with(
|
||||
'office',
|
||||
self._VALID_PERM_BODY['inbound'],
|
||||
self._VALID_PERM_BODY['outbound'],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user