test: raise coverage 68.7% -> ~80.4%; add ~250 tests for new egress/DDNS/network paths
Unit Tests / test (push) Successful in 12m6s

Coverage was below acceptable levels and several newly-added code paths
(sshuttle egress, proxy egress, DDNS provider stubs, DNS overview route,
peer-registry provisioning) had zero test coverage.

~250 new unit tests are added across 16 new test files. Existing test files
are updated to match refactored interfaces (DHCP removed, constants
introduced, network_manager restructured). .coveragerc is added to pin the
source mapping and the 70% floor so regressions are caught at commit time.

tests/test_enhanced_api.py was previously living in api/ (wrong location)
and is moved to tests/ where it belongs.

Integration test files are updated to remove references to DHCP endpoints
and add coverage for the new DNS overview and DDNS sync endpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 09:03:39 -04:00
parent c41cadafb4
commit aa1e5c41ec
33 changed files with 9446 additions and 631 deletions
+650
View File
@@ -0,0 +1,650 @@
"""
Tests for app.py: health_history (deque), health monitor logic,
connectivity endpoints, caddy endpoints, egress endpoints,
and before-request hooks (enforce_setup/enforce_auth/check_csrf).
"""
import sys
from pathlib import Path
import json
from collections import deque
from unittest.mock import patch, MagicMock
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / 'api'))
import app as app_module
from app import app
@pytest.fixture(autouse=True)
def reset_app_state():
"""Reset global mutable state between tests."""
orig_running = app_module.health_monitor_running
orig_counters = dict(app_module.service_alert_counters)
app.config['TESTING'] = True
yield
app_module.health_monitor_running = orig_running
app_module.service_alert_counters = orig_counters
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as c:
yield c
# ---------------------------------------------------------------------------
# health_history is a deque (not a list)
# ---------------------------------------------------------------------------
class TestHealthHistoryIsDeque:
def test_health_history_is_deque(self):
assert isinstance(app_module.health_history, deque)
def test_health_history_has_maxlen(self):
assert app_module.health_history.maxlen == app_module.HEALTH_HISTORY_SIZE
def test_health_history_appendleft_works(self):
"""appendleft (used in health_monitor_loop) should work on a deque."""
hh = app_module.health_history
entry = {'timestamp': '2026-01-01T00:00:00', 'alerts': []}
hh.appendleft(entry)
assert hh[0] == entry
def test_health_history_maxlen_evicts_old_entries(self):
hh = deque(maxlen=3)
for i in range(5):
hh.appendleft({'n': i})
assert len(hh) == 3
# Most recent is first
assert hh[0]['n'] == 4
# ---------------------------------------------------------------------------
# GET /api/health/history
# ---------------------------------------------------------------------------
class TestGetHealthHistory:
def test_returns_200(self, client):
with patch.object(app_module, 'health_history', deque(maxlen=100)):
resp = client.get('/api/health/history')
assert resp.status_code == 200
def test_returns_list(self, client):
with patch.object(app_module, 'health_history', deque(maxlen=100)):
resp = client.get('/api/health/history')
data = json.loads(resp.data)
assert isinstance(data, list)
def test_returns_stored_entries(self, client):
hh = deque(maxlen=100)
hh.appendleft({'timestamp': 't1', 'alerts': []})
hh.appendleft({'timestamp': 't2', 'alerts': []})
with patch.object(app_module, 'health_history', hh):
resp = client.get('/api/health/history')
data = json.loads(resp.data)
assert len(data) == 2
def test_returns_empty_when_no_history(self, client):
with patch.object(app_module, 'health_history', deque(maxlen=100)):
resp = client.get('/api/health/history')
assert json.loads(resp.data) == []
# ---------------------------------------------------------------------------
# POST /api/health/history/clear
# ---------------------------------------------------------------------------
class TestClearHealthHistory:
def test_clear_returns_200(self, client):
hh = deque(maxlen=100)
hh.appendleft({'entry': 1})
with patch.object(app_module, 'health_history', hh):
resp = client.post('/api/health/history/clear')
assert resp.status_code == 200
def test_clear_empties_history(self, client):
hh = deque(maxlen=100)
hh.appendleft({'entry': 1})
with patch.object(app_module, 'health_history', hh):
client.post('/api/health/history/clear')
assert len(hh) == 0
def test_clear_resets_alert_counters(self, client):
app_module.service_alert_counters['network'] = 5
hh = deque(maxlen=100)
with patch.object(app_module, 'health_history', hh):
client.post('/api/health/history/clear')
assert app_module.service_alert_counters == {}
def test_clear_response_has_message(self, client):
hh = deque(maxlen=100)
with patch.object(app_module, 'health_history', hh):
resp = client.post('/api/health/history/clear')
data = json.loads(resp.data)
assert 'message' in data
# ---------------------------------------------------------------------------
# perform_health_check alerting logic
# ---------------------------------------------------------------------------
class TestPerformHealthCheck:
def test_healthy_service_resets_counter(self):
app_module.service_alert_counters['network'] = 2
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['network']
network_svc = MagicMock()
network_svc.health_check.return_value = {'running': True}
mock_service_bus.get_service.return_value = network_svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = []
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
assert app_module.service_alert_counters.get('network', 0) == 0
assert 'network' in result
def test_unhealthy_service_with_error_key_increments_counter(self):
"""Services that raise an exception get recorded with an 'error' key,
which the alerting logic recognises as unhealthy."""
app_module.service_alert_counters = {}
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['network']
mock_service_bus.publish_event = MagicMock()
network_svc = MagicMock()
# Raise so the result gets {'error': ..., 'status': 'offline'}
network_svc.health_check.side_effect = Exception('container down')
mock_service_bus.get_service.return_value = network_svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = []
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
app_module.perform_health_check()
# With an 'error' key and no 'running' key, healthy=False → counter increments
assert app_module.service_alert_counters.get('network', 0) == 1
def test_alert_triggered_at_threshold(self):
"""Counter reaching HEALTH_ALERT_THRESHOLD emits an alert."""
app_module.service_alert_counters = {'network': app_module.HEALTH_ALERT_THRESHOLD - 1}
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['network']
mock_service_bus.publish_event = MagicMock()
network_svc = MagicMock()
# Use exception path to guarantee healthy=False
network_svc.health_check.side_effect = Exception('container down')
mock_service_bus.get_service.return_value = network_svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = []
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
# Alert should be in result['alerts']
assert len(result['alerts']) >= 1
assert any('network' in a for a in result['alerts'])
def test_optional_store_services_skipped_when_not_installed(self):
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['email_manager']
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = [] # email not installed
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
# email_manager should not appear in result (was skipped)
assert 'email_manager' not in result
def test_optional_store_service_checked_when_installed(self):
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['email_manager']
mock_service_bus.publish_event = MagicMock()
email_svc = MagicMock()
email_svc.health_check.return_value = {'running': True}
mock_service_bus.get_service.return_value = email_svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = ['email'] # email installed
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
assert 'email_manager' in result
def test_service_without_health_check_falls_back_to_get_status(self):
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['routing']
svc = MagicMock(spec=[]) # no health_check attribute
svc.get_status = MagicMock(return_value={'running': True})
mock_service_bus.get_service.return_value = svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = []
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
assert 'routing' in result
def test_service_exception_recorded_as_error(self):
mock_service_bus = MagicMock()
mock_service_bus.list_services.return_value = ['vault']
svc = MagicMock()
svc.health_check.side_effect = Exception('vault down')
mock_service_bus.get_service.return_value = svc
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = []
with patch.object(app_module, 'service_bus', mock_service_bus), \
patch.object(app_module, 'config_manager', mock_cfg), \
app.app_context():
result = app_module.perform_health_check()
assert 'error' in result.get('vault', {})
# ---------------------------------------------------------------------------
# GET /api/connectivity/status
# ---------------------------------------------------------------------------
class TestConnectivityEndpoints:
def test_connectivity_status_200(self, client):
mock_cm = MagicMock()
mock_cm.get_status.return_value = {'exits': [], 'peers': {}}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/status')
assert resp.status_code == 200
def test_connectivity_status_shape(self, client):
mock_cm = MagicMock()
mock_cm.get_status.return_value = {'exits': [], 'peers': {}}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/status')
data = json.loads(resp.data)
assert 'exits' in data
def test_connectivity_status_500_on_exception(self, client):
mock_cm = MagicMock()
mock_cm.get_status.side_effect = Exception('fail')
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/status')
assert resp.status_code == 500
def test_connectivity_list_exits_200(self, client):
mock_cm = MagicMock()
mock_cm.list_exits.return_value = []
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/exits')
assert resp.status_code == 200
def test_connectivity_list_exits_shape(self, client):
mock_cm = MagicMock()
mock_cm.list_exits.return_value = [{'type': 'wireguard_ext', 'name': 'exit1'}]
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/exits')
data = json.loads(resp.data)
assert 'exits' in data
assert len(data['exits']) == 1
def test_connectivity_upload_wireguard_missing_conf_text(self, client):
resp = client.post('/api/connectivity/exits/wireguard',
data=json.dumps({}), content_type='application/json')
assert resp.status_code == 400
data = json.loads(resp.data)
assert 'error' in data
def test_connectivity_upload_wireguard_empty_conf_text(self, client):
resp = client.post('/api/connectivity/exits/wireguard',
data=json.dumps({'conf_text': ' '}),
content_type='application/json')
assert resp.status_code == 400
def test_connectivity_upload_wireguard_success(self, client):
mock_cm = MagicMock()
mock_cm.upload_wireguard_ext.return_value = {'ok': True, 'message': 'Uploaded'}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.post('/api/connectivity/exits/wireguard',
data=json.dumps({'conf_text': '[Interface]\nPrivateKey = abc\n'}),
content_type='application/json')
assert resp.status_code == 200
def test_connectivity_upload_wireguard_failure(self, client):
mock_cm = MagicMock()
mock_cm.upload_wireguard_ext.return_value = {'ok': False, 'error': 'bad config'}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.post('/api/connectivity/exits/wireguard',
data=json.dumps({'conf_text': '[Interface]\nPrivateKey = abc\n'}),
content_type='application/json')
assert resp.status_code == 400
def test_connectivity_upload_openvpn_missing_ovpn_text(self, client):
resp = client.post('/api/connectivity/exits/openvpn',
data=json.dumps({}), content_type='application/json')
assert resp.status_code == 400
def test_connectivity_upload_openvpn_success(self, client):
mock_cm = MagicMock()
mock_cm.upload_openvpn.return_value = {'ok': True}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.post('/api/connectivity/exits/openvpn',
data=json.dumps({'ovpn_text': 'client\ndev tun\n'}),
content_type='application/json')
assert resp.status_code == 200
def test_connectivity_apply_routes_200(self, client):
mock_cm = MagicMock()
mock_cm.apply_routes.return_value = {'ok': True, 'applied': 0}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.post('/api/connectivity/exits/apply',
content_type='application/json')
assert resp.status_code == 200
def test_connectivity_set_peer_exit_missing_exit_via(self, client):
resp = client.put('/api/connectivity/peers/alice/exit',
data=json.dumps({}), content_type='application/json')
assert resp.status_code == 400
def test_connectivity_set_peer_exit_success(self, client):
mock_cm = MagicMock()
mock_cm.set_peer_exit.return_value = {'ok': True}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.put('/api/connectivity/peers/alice/exit',
data=json.dumps({'exit_via': 'wireguard_ext'}),
content_type='application/json')
assert resp.status_code == 200
def test_connectivity_set_peer_exit_failure(self, client):
mock_cm = MagicMock()
mock_cm.set_peer_exit.return_value = {'ok': False, 'error': 'not found'}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.put('/api/connectivity/peers/alice/exit',
data=json.dumps({'exit_via': 'wireguard_ext'}),
content_type='application/json')
assert resp.status_code == 400
def test_connectivity_get_peer_exits_200(self, client):
mock_cm = MagicMock()
mock_cm.get_peer_exits.return_value = {'alice': 'wireguard_ext'}
with patch.object(app_module, 'connectivity_manager', mock_cm):
resp = client.get('/api/connectivity/peers')
assert resp.status_code == 200
data = json.loads(resp.data)
assert 'peers' in data
# ---------------------------------------------------------------------------
# GET /api/caddy/cert-status and POST /api/caddy/cert-renew
# ---------------------------------------------------------------------------
class TestCaddyEndpoints:
def test_caddy_cert_status_200(self, client):
mock_caddy = MagicMock()
mock_caddy.get_cert_status_fresh.return_value = {'status': 'valid', 'days_remaining': 60}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.get('/api/caddy/cert-status')
assert resp.status_code == 200
def test_caddy_cert_status_shape(self, client):
mock_caddy = MagicMock()
mock_caddy.get_cert_status_fresh.return_value = {'status': 'internal'}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.get('/api/caddy/cert-status')
data = json.loads(resp.data)
assert 'status' in data
def test_caddy_cert_status_500_on_exception(self, client):
mock_caddy = MagicMock()
mock_caddy.get_cert_status_fresh.side_effect = Exception('Caddy unreachable')
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.get('/api/caddy/cert-status')
assert resp.status_code == 500
def test_caddy_cert_renew_success(self, client):
mock_caddy = MagicMock()
mock_caddy.renew_cert.return_value = {'ok': True, 'status': 'pending'}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.post('/api/caddy/cert-renew',
content_type='application/json')
assert resp.status_code == 200
def test_caddy_cert_renew_failure(self, client):
mock_caddy = MagicMock()
mock_caddy.renew_cert.return_value = {'ok': False, 'error': 'LAN mode'}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.post('/api/caddy/cert-renew',
content_type='application/json')
assert resp.status_code == 400
def test_caddy_cert_renew_500_on_exception(self, client):
mock_caddy = MagicMock()
mock_caddy.renew_cert.side_effect = Exception('fail')
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.post('/api/caddy/cert-renew',
content_type='application/json')
assert resp.status_code == 500
def test_caddy_upload_custom_cert_missing_fields(self, client):
resp = client.post('/api/caddy/custom-cert',
data=json.dumps({}), content_type='application/json')
assert resp.status_code == 400
def test_caddy_upload_custom_cert_success(self, client):
mock_caddy = MagicMock()
mock_caddy.upload_custom_cert.return_value = {'ok': True}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.post('/api/caddy/custom-cert',
data=json.dumps({'cert_pem': 'CERT', 'key_pem': 'KEY'}),
content_type='application/json')
assert resp.status_code == 200
def test_caddy_upload_custom_cert_failure(self, client):
mock_caddy = MagicMock()
mock_caddy.upload_custom_cert.return_value = {'ok': False, 'error': 'invalid cert'}
with patch.object(app_module, 'caddy_manager', mock_caddy):
resp = client.post('/api/caddy/custom-cert',
data=json.dumps({'cert_pem': 'BAD', 'key_pem': 'BAD'}),
content_type='application/json')
assert resp.status_code == 422
# ---------------------------------------------------------------------------
# GET /api/egress/status and PUT /api/egress/services/<id>/exit
# ---------------------------------------------------------------------------
class TestEgressEndpoints:
def test_egress_status_200(self, client):
mock_egress = MagicMock()
mock_egress.get_status.return_value = {'services': {}}
with patch('app.egress_manager', mock_egress, create=True):
resp = client.get('/api/egress/status')
assert resp.status_code == 200
def test_egress_status_500_on_exception(self, client):
mock_egress = MagicMock()
mock_egress.get_status.side_effect = Exception('fail')
with patch('app.egress_manager', mock_egress, create=True):
resp = client.get('/api/egress/status')
assert resp.status_code == 500
def test_egress_set_service_exit_missing_exit_type(self, client):
mock_egress = MagicMock()
with patch('app.egress_manager', mock_egress, create=True):
resp = client.put('/api/egress/services/email/exit',
data=json.dumps({}), content_type='application/json')
assert resp.status_code == 400
def test_egress_set_service_exit_success(self, client):
mock_egress = MagicMock()
mock_egress.set_service_exit.return_value = {'ok': True}
with patch('app.egress_manager', mock_egress, create=True):
resp = client.put('/api/egress/services/email/exit',
data=json.dumps({'exit_type': 'wireguard_ext'}),
content_type='application/json')
assert resp.status_code == 200
def test_egress_set_service_exit_failure(self, client):
mock_egress = MagicMock()
mock_egress.set_service_exit.return_value = {'ok': False, 'error': 'not found'}
with patch('app.egress_manager', mock_egress, create=True):
resp = client.put('/api/egress/services/email/exit',
data=json.dumps({'exit_type': 'wireguard_ext'}),
content_type='application/json')
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# enforce_setup hook: returns 428 when setup is not complete
# ---------------------------------------------------------------------------
class TestEnforceSetupHook:
def test_428_when_setup_incomplete(self):
"""Without TESTING=True, API requests are blocked if setup is not done."""
app.config['TESTING'] = False
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = False
try:
with patch.object(app_module, 'setup_manager', mock_setup):
with app.test_client() as c:
resp = c.get('/api/status')
assert resp.status_code == 428
data = json.loads(resp.data)
assert 'redirect' in data
finally:
app.config['TESTING'] = True
def test_setup_route_passes_when_incomplete(self):
"""Setup routes always pass through regardless of setup status."""
app.config['TESTING'] = False
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = False
try:
with patch.object(app_module, 'setup_manager', mock_setup):
with app.test_client() as c:
resp = c.get('/api/setup/status')
# Should NOT be 428
assert resp.status_code != 428
finally:
app.config['TESTING'] = True
def test_health_passes_when_incomplete(self):
"""The /health endpoint always passes through."""
app.config['TESTING'] = False
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = False
try:
with patch.object(app_module, 'setup_manager', mock_setup):
with app.test_client() as c:
resp = c.get('/health')
assert resp.status_code == 200
finally:
app.config['TESTING'] = True
def test_setup_complete_passes_through(self):
"""All routes pass through when setup is complete."""
app.config['TESTING'] = False
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = True
mock_auth = MagicMock()
mock_auth.list_users.return_value = []
try:
with patch.object(app_module, 'setup_manager', mock_setup), \
patch.object(app_module, 'auth_manager', mock_auth):
with app.test_client() as c:
resp = c.get('/api/status')
assert resp.status_code != 428
finally:
app.config['TESTING'] = True
# ---------------------------------------------------------------------------
# enforce_auth hook: 503 when users file exists but is empty
# ---------------------------------------------------------------------------
class TestEnforceAuthHook:
def test_503_when_users_file_empty_and_readable(self, tmp_path):
"""Returns 503 when users file exists + readable but has no accounts."""
import tempfile, os
app.config['TESTING'] = False
users_file = tmp_path / 'auth_users.json'
users_file.write_text('[]') # file exists but no accounts
from auth_manager import AuthManager
real_auth = MagicMock(spec=AuthManager)
real_auth.list_users.return_value = []
real_auth._users_file = str(users_file)
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = True
try:
with patch.object(app_module, 'auth_manager', real_auth), \
patch.object(app_module, 'setup_manager', mock_setup):
with app.test_client() as c:
resp = c.get('/api/status')
assert resp.status_code == 503
data = json.loads(resp.data)
assert 'error' in data
finally:
app.config['TESTING'] = True
def test_401_when_no_session_and_users_exist(self, tmp_path):
"""Returns 401 when users exist but no session cookie is set."""
app.config['TESTING'] = False
users_file = tmp_path / 'auth_users.json'
# Users file doesn't exist — no file means enforcement
# is bypassed. Use a file that DOES have a user.
import json as _json
users_file.write_text(_json.dumps([{'username': 'admin', 'role': 'admin'}]))
from auth_manager import AuthManager
real_auth = MagicMock(spec=AuthManager)
real_auth.list_users.return_value = [{'username': 'admin', 'role': 'admin'}]
real_auth._users_file = str(users_file)
mock_setup = MagicMock()
mock_setup.is_setup_complete.return_value = True
try:
with patch.object(app_module, 'auth_manager', real_auth), \
patch.object(app_module, 'setup_manager', mock_setup):
with app.test_client() as c:
# No login — no session
resp = c.get('/api/status')
assert resp.status_code == 401
finally:
app.config['TESTING'] = True
# ---------------------------------------------------------------------------
# GET /api/status
# ---------------------------------------------------------------------------
class TestGetCellStatus:
def test_returns_200(self, client):
mock_sb = MagicMock()
mock_sb.list_services.return_value = []
mock_pr = MagicMock()
mock_pr.list_peers.return_value = []
mock_cm = MagicMock()
mock_cm.configs = {'_identity': {'cell_name': 'test', 'domain': 'cell'}}
mock_cm.get_effective_domain.return_value = 'cell'
with patch.object(app_module, 'service_bus', mock_sb), \
patch.object(app_module, 'peer_registry', mock_pr), \
patch.object(app_module, 'config_manager', mock_cm):
resp = client.get('/api/status')
assert resp.status_code == 200
def test_status_includes_expected_keys(self, client):
mock_sb = MagicMock()
mock_sb.list_services.return_value = []
mock_pr = MagicMock()
mock_pr.list_peers.return_value = []
mock_cm = MagicMock()
mock_cm.configs = {'_identity': {'cell_name': 'test', 'domain': 'cell'}}
mock_cm.get_effective_domain.return_value = 'cell'
with patch.object(app_module, 'service_bus', mock_sb), \
patch.object(app_module, 'peer_registry', mock_pr), \
patch.object(app_module, 'config_manager', mock_cm):
resp = client.get('/api/status')
data = json.loads(resp.data)
for key in ('cell_name', 'domain', 'uptime', 'peers_count', 'services'):
assert key in data, f"Missing key: {key}"