feat(cells): Phase 2 — exit-offer signaling between connected cells
Adds the ability for a cell to signal to a peer that it's willing to route internet traffic on their behalf. This is the signaling layer for Phase 3 (per-peer routing via exit cell). Changes: - cell_links.json: exit_offered (bool) + remote_exit_offered (bool) fields with lazy migration (default false for existing records) - _push_permissions_to_remote: includes exit_offered in the push body - apply_remote_permissions: accepts exit_offered kwarg; stores it as remote_exit_offered on the matching cell link - peer-sync receiver: passes exit_offered from body to apply_remote_permissions - CellLinkManager.set_exit_offered(cell_name, offered): persists + triggers push so the remote learns of our offer immediately - PUT /api/cells/<name>/exit-offer: REST endpoint to toggle the flag - 12 new tests covering all new paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -570,7 +570,8 @@ class TestPeerSyncPermissionsEndpoint(unittest.TestCase):
|
||||
environ_base={'REMOTE_ADDR': '10.1.0.5'},
|
||||
)
|
||||
mock_clm.apply_remote_permissions.assert_called_once_with(
|
||||
'officepubkey=', self._VALID_BODY['permissions']
|
||||
'officepubkey=', self._VALID_BODY['permissions'],
|
||||
exit_offered=False,
|
||||
)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
@@ -685,5 +686,55 @@ class TestPeerSyncPermissionsEndpoint(unittest.TestCase):
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
|
||||
class TestSetExitOffer(unittest.TestCase):
|
||||
"""PUT /api/cells/<cell_name>/exit-offer"""
|
||||
|
||||
def setUp(self):
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_set_exit_offer_true_calls_set_exit_offered(self, mock_clm):
|
||||
mock_clm.set_exit_offered.return_value = {'cell_name': 'remote', 'exit_offered': True}
|
||||
r = self.client.put(
|
||||
'/api/cells/remote/exit-offer',
|
||||
data=json.dumps({'exit_offered': True}),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
mock_clm.set_exit_offered.assert_called_once_with('remote', True)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_set_exit_offer_false_calls_set_exit_offered(self, mock_clm):
|
||||
mock_clm.set_exit_offered.return_value = {'cell_name': 'remote', 'exit_offered': False}
|
||||
r = self.client.put(
|
||||
'/api/cells/remote/exit-offer',
|
||||
data=json.dumps({'exit_offered': False}),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
mock_clm.set_exit_offered.assert_called_once_with('remote', False)
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_set_exit_offer_missing_field_returns_400(self, mock_clm):
|
||||
r = self.client.put(
|
||||
'/api/cells/remote/exit-offer',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
mock_clm.set_exit_offered.assert_not_called()
|
||||
|
||||
@patch('app.cell_link_manager')
|
||||
def test_set_exit_offer_unknown_cell_returns_404(self, mock_clm):
|
||||
mock_clm.set_exit_offered.side_effect = ValueError("Cell 'nobody' not found")
|
||||
r = self.client.put(
|
||||
'/api/cells/nobody/exit-offer',
|
||||
data=json.dumps({'exit_offered': True}),
|
||||
content_type='application/json',
|
||||
)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user