feat: connectivity redesign phase 2 — instance-aware routing + reference connections by id
Unit Tests / test (push) Successful in 12m6s

apply_routes now iterates over connection instances rather than types:
each instance gets its own fwmark, routing table, interface, and
redirect_port via _routing_connections / _resolve_peer_connection /
_apply_connection_for_src; kill-switch is enforced per iface-instance.
Old per-type MARKS/TABLES constants are kept only as migration scaffolding.

peer_registry: exit_via is now stored as a connection id (or 'default');
_migrate_exit_via_to_connection_id runs on _load_peers to upgrade legacy
type-string values; set_peer_exit_via validates against known connection
ids; VALID_EXIT_VIA removed; config_manager wired in from managers.py.

egress_manager: egress_overrides keyed by service_id → connection_id;
local MARKS/TABLES/EXIT_TYPES/_REDIRECT_PORTS/_add_tor_redirect removed;
(mark, table, redirect_port) resolved at apply-time via
connectivity_manager.get_connection; manifest egress.allowed still
enforced by connection type.

api/app.py + api.js: PUT peer/service exit endpoints accept {connection_id};
back-compat shim resolves a legacy type string to its single active instance.

Tests extended: two same-type instances produce distinct marks/tables/ports;
peer exit_via and egress override id migrations round-trip correctly;
single-instance behaviour is equivalent to the old type-keyed path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:35:28 -04:00
parent 5b9d20eeac
commit 89aed4efe0
12 changed files with 993 additions and 375 deletions
+192 -120
View File
@@ -1,49 +1,85 @@
"""
Tests for EgressManager — per-service egress enforcement via host iptables.
Connectivity v2: a service routes through a connection *instance* (by id),
sharing the connection's fwmark / routing table / redirect port. The egress
override map is service_id → connection_id, and (mark, table, redirect_port)
are resolved from ConnectivityManager.get_connection(id). EgressManager no
longer owns its own per-type MARKS/TABLES.
All subprocess calls (iptables, iptables-save, iptables-restore, ip rule,
docker inspect) and config_manager state are mocked so these tests run
without any live infrastructure or root privileges.
docker inspect) and manager state are mocked so these tests run without any
live infrastructure or root privileges.
"""
import os
import sys
import unittest
from unittest.mock import MagicMock, patch, call
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api'))
import egress_manager as em_module
from egress_manager import EgressManager, MARKS, TABLES, EXIT_TYPES, EGRESS_CHAIN
from egress_manager import EgressManager, EGRESS_CHAIN
# ---------------------------------------------------------------------------
# Helpers
# Connection fixtures — mirror the v2 allocator output.
# ---------------------------------------------------------------------------
def _make_manager(installed=None, overrides=None):
"""Build an EgressManager backed by a mock config_manager."""
CONNECTIONS = {
'conn_wg': {
'id': 'conn_wg', 'type': 'wireguard_ext', 'name': 'Work VPN',
'enabled': True, 'mark': 0x1000, 'table': 1000,
'iface': 'wgext_aaaa', 'redirect_port': None,
},
'conn_ovpn': {
'id': 'conn_ovpn', 'type': 'openvpn', 'name': 'OVPN',
'enabled': True, 'mark': 0x1010, 'table': 1001,
'iface': 'ovpn_bbbb', 'redirect_port': None,
},
'conn_tor': {
'id': 'conn_tor', 'type': 'tor', 'name': 'Tor',
'enabled': True, 'mark': 0x1020, 'table': 1002,
'iface': None, 'redirect_port': 9100,
},
}
def _make_manager(installed=None, overrides=None, connections=None):
"""Build an EgressManager backed by mock config + connectivity managers."""
cm = MagicMock()
cm.get_installed_services.return_value = installed or {}
# Wire up configs dict so _get_egress_overrides / _set_egress_overrides work
cm.configs = {'egress_overrides': overrides or {}}
cm._save_all_configs = MagicMock()
return EgressManager(config_manager=cm), cm
conns = connections if connections is not None else CONNECTIONS
conn_list = list(conns.values())
cm.list_connections.return_value = conn_list
cm.get_connection.side_effect = lambda cid: conns.get(cid)
connectivity = MagicMock()
connectivity.list_connections.return_value = conn_list
connectivity.get_connection.side_effect = lambda cid: conns.get(cid)
mgr = EgressManager(config_manager=cm, connectivity_manager=connectivity)
return mgr, cm
def _subprocess_ok(stdout=''):
"""Return a MagicMock simulating a successful subprocess.run result."""
return MagicMock(returncode=0, stdout=stdout, stderr='')
def _subprocess_fail(stderr='error', stdout=''):
"""Return a MagicMock simulating a failed subprocess.run result."""
return MagicMock(returncode=1, stdout=stdout, stderr=stderr)
def _make_manifest(has_egress=True, egress_default='wireguard_ext',
def _make_manifest(has_egress=True, egress_default='conn_wg',
allowed=None, container_name='cell-myapp'):
"""Return a minimal manifest dict with optional egress configuration."""
"""Return a minimal manifest dict with optional egress configuration.
`allowed` is a list of connection *types* (manifests are type-scoped).
"""
m = {
'id': 'myapp',
'name': 'My App',
@@ -53,7 +89,8 @@ def _make_manifest(has_egress=True, egress_default='wireguard_ext',
m['has_egress'] = True
m['egress'] = {
'default': egress_default,
'allowed': allowed if allowed is not None else list(EXIT_TYPES),
'allowed': allowed if allowed is not None
else ['wireguard_ext', 'openvpn', 'tor', 'sshuttle', 'proxy'],
}
else:
m['has_egress'] = False
@@ -61,32 +98,26 @@ def _make_manifest(has_egress=True, egress_default='wireguard_ext',
def _installed_with_manifest(manifest, service_id='myapp'):
"""Return an installed-services dict containing one service record."""
return {service_id: {'id': service_id, 'manifest': manifest}}
# ---------------------------------------------------------------------------
# 1. test_apply_service_default_exit_no_iptables_calls
# 1. default exitno iptables rule-adding
# ---------------------------------------------------------------------------
class TestApplyServiceDefaultExit(unittest.TestCase):
def test_apply_service_default_exit_no_iptables_calls(self):
"""When egress.default is 'default', apply_service must not touch iptables."""
manifest = _make_manifest(egress_default='default')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
with patch('subprocess.run') as mock_run:
# docker inspect must return an IP so we don't fail earlier
mock_run.return_value = _subprocess_ok(stdout='172.20.0.50\n')
result = mgr.apply_service('myapp')
self.assertTrue(result['ok'])
self.assertEqual(result.get('exit_via'), 'default')
# No iptables rule-insertion or mark call should have been made.
# iptables-save from clear_service is allowed; we only check that
# no iptables -A / -I (rule-adding) calls were made.
rule_add_calls = [
c for c in mock_run.call_args_list
if c.args and c.args[0][:1] == ['iptables']
@@ -96,30 +127,25 @@ class TestApplyServiceDefaultExit(unittest.TestCase):
# ---------------------------------------------------------------------------
# 2. test_apply_service_wireguard_ext_adds_mark_rule
# 2. wireguard_ext connection → mark rule with the connection's own mark
# ---------------------------------------------------------------------------
class TestApplyServiceWireguardExt(unittest.TestCase):
def test_apply_service_wireguard_ext_adds_mark_rule(self):
"""wireguard_ext exit must add a mangle MARK rule with 0x110 and the correct comment."""
manifest = _make_manifest(egress_default='wireguard_ext')
manifest = _make_manifest(egress_default='conn_wg')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
calls_made = []
def fake_run(cmd, **kwargs):
calls_made.append(cmd)
# docker inspect → return IP
if 'docker' in cmd and 'inspect' in cmd:
return _subprocess_ok(stdout='172.20.0.50\n')
# iptables-save → empty ruleset
if 'iptables-save' in cmd:
return _subprocess_ok(stdout='')
# iptables-restore → success
if 'iptables-restore' in cmd:
return _subprocess_ok()
# ip rule del → fail (none to delete)
if cmd[:3] == ['ip', 'rule', 'del']:
return _subprocess_fail()
return _subprocess_ok()
@@ -128,29 +154,35 @@ class TestApplyServiceWireguardExt(unittest.TestCase):
result = mgr.apply_service('myapp')
self.assertTrue(result['ok'], result)
self.assertEqual(result['exit_via'], 'wireguard_ext')
self.assertEqual(result['exit_via'], 'conn_wg')
# Find the mangle MARK -A call
mark_calls = [
c for c in calls_made
if 'iptables' in str(c) and 'MARK' in c and '--set-mark' in c
]
self.assertGreater(len(mark_calls), 0, 'No MARK rule was added')
mark_cmd = ' '.join(mark_calls[0])
self.assertIn('0x110', mark_cmd)
self.assertIn('0x1000', mark_cmd) # the connection's mark
self.assertIn('pic-egr-myapp', mark_cmd)
self.assertIn('mangle', mark_cmd)
# The ip rule must point fwmark→the connection's table.
ip_rule_add = [
c for c in calls_made
if c[:3] == ['ip', 'rule', 'add']
]
self.assertGreater(len(ip_rule_add), 0)
self.assertIn('1000', ' '.join(ip_rule_add[0])) # table
# ---------------------------------------------------------------------------
# 3. test_apply_service_openvpn_adds_mark_rule
# 3. openvpn connection → its own mark
# ---------------------------------------------------------------------------
class TestApplyServiceOpenVPN(unittest.TestCase):
def test_apply_service_openvpn_adds_mark_rule(self):
"""openvpn exit must add a mangle MARK rule with 0x120."""
manifest = _make_manifest(egress_default='openvpn')
manifest = _make_manifest(egress_default='conn_ovpn')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
calls_made = []
@@ -171,25 +203,24 @@ class TestApplyServiceOpenVPN(unittest.TestCase):
result = mgr.apply_service('myapp')
self.assertTrue(result['ok'], result)
self.assertEqual(result['exit_via'], 'openvpn')
self.assertEqual(result['exit_via'], 'conn_ovpn')
mark_calls = [
c for c in calls_made
if 'iptables' in str(c) and 'MARK' in c and '--set-mark' in c
]
self.assertGreater(len(mark_calls), 0)
self.assertIn('0x120', ' '.join(mark_calls[0]))
self.assertIn('0x1010', ' '.join(mark_calls[0]))
# ---------------------------------------------------------------------------
# 4. test_apply_service_tor_adds_mark_and_redirect
# 4. tor (redirect-style) connection → mark + REDIRECT to its redirect_port
# ---------------------------------------------------------------------------
class TestApplyServiceTor(unittest.TestCase):
def test_apply_service_tor_adds_mark_and_redirect(self):
"""tor exit must add a mangle MARK 0x130 AND a nat REDIRECT to port 9040."""
manifest = _make_manifest(egress_default='tor')
manifest = _make_manifest(egress_default='conn_tor')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
calls_made = []
@@ -210,14 +241,14 @@ class TestApplyServiceTor(unittest.TestCase):
result = mgr.apply_service('myapp')
self.assertTrue(result['ok'], result)
self.assertEqual(result['exit_via'], 'tor')
self.assertEqual(result['exit_via'], 'conn_tor')
mark_calls = [
c for c in calls_made
if 'iptables' in str(c) and 'MARK' in c and '--set-mark' in c
]
self.assertGreater(len(mark_calls), 0, 'No MARK rule found')
self.assertIn('0x130', ' '.join(mark_calls[0]))
self.assertIn('0x1020', ' '.join(mark_calls[0]))
redirect_calls = [
c for c in calls_made
@@ -225,24 +256,23 @@ class TestApplyServiceTor(unittest.TestCase):
]
self.assertGreater(len(redirect_calls), 0, 'No REDIRECT rule found')
redirect_cmd = ' '.join(redirect_calls[0])
self.assertIn('9040', redirect_cmd)
self.assertIn('9100', redirect_cmd) # the connection's redirect_port
self.assertIn('nat', redirect_cmd)
# ---------------------------------------------------------------------------
# 5. test_apply_service_no_container_ip_returns_error
# 5. no container IP → error
# ---------------------------------------------------------------------------
class TestApplyServiceNoContainerIP(unittest.TestCase):
def test_apply_service_no_container_ip_returns_error(self):
"""When docker inspect returns an empty IP, apply_service must return ok=False."""
manifest = _make_manifest(egress_default='wireguard_ext')
manifest = _make_manifest(egress_default='conn_wg')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
def fake_run(cmd, **kwargs):
if 'docker' in cmd and 'inspect' in cmd:
return _subprocess_ok(stdout='\n') # empty IP
return _subprocess_ok(stdout='\n')
if 'iptables-save' in cmd:
return _subprocess_ok(stdout='')
if 'iptables-restore' in cmd:
@@ -257,14 +287,13 @@ class TestApplyServiceNoContainerIP(unittest.TestCase):
# ---------------------------------------------------------------------------
# 6. test_apply_service_container_ip_retries
# 6. container IP retries
# ---------------------------------------------------------------------------
class TestApplyServiceRetries(unittest.TestCase):
def test_apply_service_container_ip_retries(self):
"""First docker inspect attempt fails; second succeeds — result must be ok=True."""
manifest = _make_manifest(egress_default='wireguard_ext')
manifest = _make_manifest(egress_default='conn_wg')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
inspect_count = [0]
@@ -273,8 +302,8 @@ class TestApplyServiceRetries(unittest.TestCase):
if 'docker' in cmd and 'inspect' in cmd:
inspect_count[0] += 1
if inspect_count[0] == 1:
return _subprocess_ok(stdout='\n') # first attempt: empty
return _subprocess_ok(stdout='172.20.0.50\n') # second: success
return _subprocess_ok(stdout='\n')
return _subprocess_ok(stdout='172.20.0.50\n')
if 'iptables-save' in cmd:
return _subprocess_ok(stdout='')
if 'iptables-restore' in cmd:
@@ -284,7 +313,7 @@ class TestApplyServiceRetries(unittest.TestCase):
return _subprocess_ok()
with patch('subprocess.run', side_effect=fake_run):
with patch('time.sleep'): # skip actual delays
with patch('time.sleep'):
result = mgr.apply_service('myapp')
self.assertTrue(result['ok'], result)
@@ -292,13 +321,12 @@ class TestApplyServiceRetries(unittest.TestCase):
# ---------------------------------------------------------------------------
# 7. test_has_egress_false_skips_rules
# 7. has_egress Falseskipped
# ---------------------------------------------------------------------------
class TestHasEgressFalse(unittest.TestCase):
def test_has_egress_false_skips_rules(self):
"""A manifest with has_egress=False must skip rules and return skipped=True."""
manifest = _make_manifest(has_egress=False)
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
@@ -309,8 +337,6 @@ class TestHasEgressFalse(unittest.TestCase):
self.assertTrue(result['ok'])
self.assertTrue(result.get('skipped'))
# No iptables rule-insertion call should have been made.
# iptables-save from clear_service is permitted; only check no -A/-I.
rule_add_calls = [
c for c in mock_run.call_args_list
if c.args and c.args[0][:1] == ['iptables']
@@ -320,22 +346,18 @@ class TestHasEgressFalse(unittest.TestCase):
# ---------------------------------------------------------------------------
# 8. test_has_egress_missing_egress_block_skips
# 8. has_egress True but no egress blockskipped
# ---------------------------------------------------------------------------
class TestHasEgressMissingBlock(unittest.TestCase):
def test_has_egress_missing_egress_block_skips(self):
"""has_egress=True but no 'egress' dict → must skip (skipped=True)."""
manifest = {
'id': 'myapp',
'container_name': 'cell-myapp',
'has_egress': True,
# 'egress' key intentionally absent
}
mgr, _ = _make_manager(
installed=_installed_with_manifest(manifest)
)
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
with patch('subprocess.run') as mock_run:
mock_run.return_value = _subprocess_ok(stdout='')
@@ -346,23 +368,21 @@ class TestHasEgressMissingBlock(unittest.TestCase):
# ---------------------------------------------------------------------------
# 9. test_clear_service_removes_tagged_rules
# 9. clear_service removes only the tagged rules
# ---------------------------------------------------------------------------
class TestClearService(unittest.TestCase):
def test_clear_service_removes_tagged_rules(self):
"""iptables-restore is called with the tagged lines removed."""
mgr, _ = _make_manager()
mangle_rules = (
'-A PIC_EGRESS -s 172.20.0.50 -j MARK --set-mark 0x110 '
'-A PIC_EGRESS -s 172.20.0.50 -j MARK --set-mark 0x1000 '
'-m comment --comment "pic-egr-myapp"\n'
'-A PIC_EGRESS -s 172.20.0.99 -j MARK --set-mark 0x110 '
'-A PIC_EGRESS -s 172.20.0.99 -j MARK --set-mark 0x1000 '
'-m comment --comment "pic-egr-otherapp"\n'
)
nat_rules = ''
restore_inputs = {}
def fake_run(cmd, input=None, **kwargs):
@@ -382,28 +402,25 @@ class TestClearService(unittest.TestCase):
result = mgr.clear_service('myapp')
self.assertTrue(result['ok'])
# The restored mangle rules must not contain myapp's tag
restored = restore_inputs.get('mangle', '')
self.assertNotIn('pic-egr-myapp', restored)
# But the other service's rules must be preserved
self.assertIn('pic-egr-otherapp', restored)
# ---------------------------------------------------------------------------
# 10. test_set_service_exit_rejects_not_in_allowed
# 10. set_service_exit rejects a connection whose type is not in allowed
# ---------------------------------------------------------------------------
class TestSetServiceExitRejectNotAllowed(unittest.TestCase):
def test_set_service_exit_rejects_not_in_allowed(self):
"""Exit type not in manifest's allowed list must return ok=False."""
def test_set_service_exit_rejects_type_not_in_allowed(self):
manifest = _make_manifest(
egress_default='default',
allowed=['default', 'tor'], # wireguard_ext not in allowed
allowed=['tor'], # wireguard_ext not allowed
)
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
result = mgr.set_service_exit('myapp', 'wireguard_ext')
result = mgr.set_service_exit('myapp', 'conn_wg')
self.assertFalse(result['ok'])
self.assertIn('error', result)
@@ -411,47 +428,71 @@ class TestSetServiceExitRejectNotAllowed(unittest.TestCase):
# ---------------------------------------------------------------------------
# 11. test_set_service_exit_persists_and_applies
# 11. set_service_exit persists the connection id and applies
# ---------------------------------------------------------------------------
class TestSetServiceExitPersistsAndApplies(unittest.TestCase):
def test_set_service_exit_persists_and_applies(self):
"""Valid override must be persisted to config_manager and apply_service called."""
manifest = _make_manifest(egress_default='default', allowed=list(EXIT_TYPES))
manifest = _make_manifest(egress_default='default')
mgr, cm = _make_manager(installed=_installed_with_manifest(manifest))
apply_calls = []
original_apply = mgr.apply_service
mgr.apply_service = lambda sid: apply_calls.append(sid) or {
'ok': True, 'exit_via': 'conn_tor'}
def fake_apply(sid):
apply_calls.append(sid)
return {'ok': True, 'exit_via': 'tor'}
result = mgr.set_service_exit('myapp', 'conn_tor')
mgr.apply_service = fake_apply
self.assertTrue(result['ok'], result)
self.assertIn('myapp', apply_calls)
cm._save_all_configs.assert_called()
self.assertEqual(cm.configs['egress_overrides'].get('myapp'), 'conn_tor')
def test_set_service_exit_default_clears_override(self):
manifest = _make_manifest(egress_default='conn_wg')
mgr, cm = _make_manager(
installed=_installed_with_manifest(manifest),
overrides={'myapp': 'conn_tor'},
)
mgr.apply_service = lambda sid: {'ok': True, 'exit_via': 'default'}
result = mgr.set_service_exit('myapp', 'default')
self.assertTrue(result['ok'])
self.assertEqual(cm.configs['egress_overrides'].get('myapp'), 'default')
def test_set_service_exit_unknown_connection_rejected(self):
manifest = _make_manifest(egress_default='default')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
result = mgr.set_service_exit('myapp', 'conn_ghost')
self.assertFalse(result['ok'])
self.assertIn('unknown connection', result['error'])
def test_set_service_exit_legacy_type_resolves_to_single_instance(self):
"""Back-compat shim: a legacy type resolves to the one instance of it."""
manifest = _make_manifest(egress_default='default', allowed=['tor'])
mgr, cm = _make_manager(installed=_installed_with_manifest(manifest))
mgr.apply_service = lambda sid: {'ok': True}
result = mgr.set_service_exit('myapp', 'tor')
self.assertTrue(result['ok'], result)
# apply_service was called
self.assertIn('myapp', apply_calls)
# override was persisted
cm._save_all_configs.assert_called()
self.assertEqual(cm.configs['egress_overrides'].get('myapp'), 'tor')
self.assertEqual(cm.configs['egress_overrides'].get('myapp'), 'conn_tor')
# ---------------------------------------------------------------------------
# 12. test_apply_all_iterates_installed_services
# 12. apply_all iterates installed services
# ---------------------------------------------------------------------------
class TestApplyAll(unittest.TestCase):
def test_apply_all_iterates_installed_services(self):
"""apply_all must call apply_service for every service with a manifest."""
manifests = {
'svc1': _make_manifest(egress_default='wireguard_ext'),
'svc2': _make_manifest(egress_default='openvpn'),
'svc3': _make_manifest(egress_default='tor'),
'svc1': _make_manifest(egress_default='conn_wg'),
'svc2': _make_manifest(egress_default='conn_ovpn'),
'svc3': _make_manifest(egress_default='conn_tor'),
}
installed = {
sid: {'id': sid, 'manifest': m}
@@ -469,37 +510,57 @@ class TestApplyAll(unittest.TestCase):
# ---------------------------------------------------------------------------
# 13. test_marks_do_not_collide_with_connectivity_manager
# 13. service + peer on the same connection share the same mark
# ---------------------------------------------------------------------------
class TestMarksNoCollision(unittest.TestCase):
class TestSharedMarkWithConnection(unittest.TestCase):
def test_marks_do_not_collide_with_connectivity_manager(self):
"""EgressManager marks must be disjoint from ConnectivityManager marks."""
connectivity_marks = {0x10, 0x20, 0x30}
egress_mark_values = set(MARKS.values())
collision = connectivity_marks & egress_mark_values
self.assertEqual(
collision, set(),
f'Mark collision with ConnectivityManager: {collision}',
)
def test_service_uses_connection_mark_not_a_private_table(self):
"""A service routed via a connection uses that connection's mark/table,
i.e. the SAME resources a peer on that connection would use."""
manifest = _make_manifest(egress_default='conn_wg')
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
captured = {}
def fake_run(cmd, **kwargs):
if 'docker' in cmd and 'inspect' in cmd:
return _subprocess_ok(stdout='172.20.0.50\n')
if 'iptables-save' in cmd:
return _subprocess_ok(stdout='')
if 'iptables-restore' in cmd:
return _subprocess_ok()
if cmd[:3] == ['ip', 'rule', 'del']:
return _subprocess_fail()
if cmd[:3] == ['ip', 'rule', 'add']:
captured['ip_rule'] = ' '.join(cmd)
if 'MARK' in cmd:
captured['mark'] = ' '.join(cmd)
return _subprocess_ok()
with patch('subprocess.run', side_effect=fake_run):
mgr.apply_service('myapp')
# conn_wg has mark 0x1000 / table 1000 — both must appear, proving the
# service inherits the connection's shared routing resources.
self.assertIn('0x1000', captured.get('mark', ''))
self.assertIn('1000', captured.get('ip_rule', ''))
# ---------------------------------------------------------------------------
# 14. test_apply_service_unknown_exit_in_allowed_rejected
# 14. apply_service with an unknown connection id → error
# ---------------------------------------------------------------------------
class TestApplyServiceUnknownExit(unittest.TestCase):
def test_apply_service_unknown_exit_in_allowed_rejected(self):
"""An egress.default value that is not a known EXIT_TYPE must return ok=False."""
def test_apply_service_unknown_connection_rejected(self):
manifest = {
'id': 'myapp',
'container_name': 'cell-myapp',
'has_egress': True,
'egress': {
'default': 'internet_fast_lane', # unknown exit
'allowed': ['internet_fast_lane'],
'default': 'conn_ghost', # no such connection
'allowed': ['wireguard_ext'],
},
}
mgr, _ = _make_manager(installed=_installed_with_manifest(manifest))
@@ -521,7 +582,7 @@ class TestApplyServiceUnknownExit(unittest.TestCase):
# ---------------------------------------------------------------------------
# Additional coverage: _has_egress edge cases
# _has_egress edge cases
# ---------------------------------------------------------------------------
class TestHasEgressLogic(unittest.TestCase):
@@ -530,16 +591,15 @@ class TestHasEgressLogic(unittest.TestCase):
self.mgr, _ = _make_manager()
def test_has_egress_both_required(self):
"""Both has_egress=True and non-empty egress dict required."""
m = {'has_egress': True, 'egress': {'default': 'tor', 'allowed': ['tor']}}
m = {'has_egress': True, 'egress': {'default': 'conn_tor', 'allowed': ['tor']}}
self.assertTrue(self.mgr._has_egress(m))
def test_has_egress_false_field(self):
m = {'has_egress': False, 'egress': {'default': 'tor', 'allowed': ['tor']}}
m = {'has_egress': False, 'egress': {'default': 'conn_tor', 'allowed': ['tor']}}
self.assertFalse(self.mgr._has_egress(m))
def test_has_egress_missing_has_egress_key(self):
m = {'egress': {'default': 'tor', 'allowed': ['tor']}}
m = {'egress': {'default': 'conn_tor', 'allowed': ['tor']}}
self.assertFalse(self.mgr._has_egress(m))
def test_has_egress_empty_egress_dict(self):
@@ -548,29 +608,41 @@ class TestHasEgressLogic(unittest.TestCase):
# ---------------------------------------------------------------------------
# Additional coverage: _resolve_exit
# _resolve_exit (now returns connection ids)
# ---------------------------------------------------------------------------
class TestResolveExit(unittest.TestCase):
def test_override_takes_precedence(self):
mgr, _ = _make_manager(overrides={'myapp': 'openvpn'})
manifest = _make_manifest(egress_default='wireguard_ext')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'openvpn')
mgr, _ = _make_manager(overrides={'myapp': 'conn_ovpn'})
manifest = _make_manifest(egress_default='conn_wg')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'conn_ovpn')
def test_manifest_default_used_when_no_override(self):
mgr, _ = _make_manager(overrides={})
manifest = _make_manifest(egress_default='tor')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'tor')
manifest = _make_manifest(egress_default='conn_tor')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'conn_tor')
def test_fallback_to_default_when_no_egress_block(self):
mgr, _ = _make_manager(overrides={})
manifest = {'id': 'myapp'}
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'default')
def test_legacy_type_override_migrates_to_connection_id(self):
"""An old override holding a type string resolves to the migrated id."""
mgr, _ = _make_manager(overrides={'myapp': 'wireguard_ext'})
manifest = _make_manifest(egress_default='default')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'conn_wg')
def test_legacy_type_default_with_no_instance_falls_back(self):
"""A legacy type with no matching instance falls back to 'default'."""
mgr, _ = _make_manager(connections={})
manifest = _make_manifest(egress_default='tor')
self.assertEqual(mgr._resolve_exit('myapp', manifest), 'default')
# ---------------------------------------------------------------------------
# Additional: apply_service with missing manifest
# apply_service with missing manifest
# ---------------------------------------------------------------------------
class TestApplyServiceMissingManifest(unittest.TestCase):