feat: Phase 4 — dynamic nav + service visibility based on installed services
Unit Tests / test (push) Successful in 11m24s
Unit Tests / test (push) Successful in 11m24s
Email, calendar, and files no longer appear in the nav or as usable pages unless they are installed. The nav refreshes whenever a service is installed or removed via the new pic-services-changed CustomEvent. Changes: - routes/services.py: add GET /api/services/active endpoint - api.js: add servicesAPI.listActive() - App.jsx: replace hardcoded coreServiceChildren with dynamic state fetched from /api/services/active; SERVICE_META maps ids to nav entry shapes - ServiceNotInstalledBanner.jsx: new component — admin gets catalog link, peer gets "contact admin" message - EmailPage/CalendarPage/FilesPage: show banner when service not installed - ServicesIndex.jsx: remove CoreServiceCard + CORE_SERVICES "Built-in" section; rename Remove → Uninstall; dispatch pic-services-changed on install/uninstall success - MyServices.jsx: conditionally render service cards based on active list; placeholder card when absent; page-level notice when nothing is installed - tests/test_services_active_endpoint.py: 4 new endpoint tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Tests for GET /api/services/active endpoint.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'api'))
|
||||
|
||||
from app import app
|
||||
|
||||
|
||||
def _make_registry(active_services):
|
||||
reg = MagicMock()
|
||||
reg.list_active = MagicMock(return_value=active_services)
|
||||
return reg
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client():
|
||||
app.config['TESTING'] = True
|
||||
with app.test_client() as c:
|
||||
yield c
|
||||
|
||||
|
||||
def test_active_returns_200(client):
|
||||
import app as app_module
|
||||
with patch.object(app_module, 'service_registry', _make_registry([])):
|
||||
resp = client.get('/api/services/active')
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_active_returns_empty_list_when_nothing_installed(client):
|
||||
import app as app_module
|
||||
with patch.object(app_module, 'service_registry', _make_registry([])):
|
||||
resp = client.get('/api/services/active')
|
||||
data = json.loads(resp.data)
|
||||
assert data == []
|
||||
|
||||
|
||||
def test_active_returns_installed_services(client):
|
||||
email_svc = {
|
||||
'id': 'email',
|
||||
'name': 'Email',
|
||||
'subdomain': 'mail',
|
||||
'capabilities': {'has_accounts': True},
|
||||
'config': {},
|
||||
}
|
||||
import app as app_module
|
||||
with patch.object(app_module, 'service_registry', _make_registry([email_svc])):
|
||||
resp = client.get('/api/services/active')
|
||||
data = json.loads(resp.data)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 1
|
||||
assert data[0]['id'] == 'email'
|
||||
assert data[0]['name'] == 'Email'
|
||||
|
||||
|
||||
def test_active_response_shape(client):
|
||||
"""Each entry must have id, name, subdomain, and capabilities keys."""
|
||||
email_svc = {
|
||||
'id': 'email',
|
||||
'name': 'Email',
|
||||
'subdomain': 'mail',
|
||||
'capabilities': {'has_accounts': True},
|
||||
'config': {},
|
||||
}
|
||||
import app as app_module
|
||||
with patch.object(app_module, 'service_registry', _make_registry([email_svc])):
|
||||
resp = client.get('/api/services/active')
|
||||
data = json.loads(resp.data)
|
||||
entry = data[0]
|
||||
assert 'id' in entry
|
||||
assert 'name' in entry
|
||||
assert 'subdomain' in entry
|
||||
assert 'capabilities' in entry
|
||||
Reference in New Issue
Block a user