feat: add comprehensive E2E test suite (Playwright + WireGuard + API)
Adds tests/e2e/ with three layers of E2E coverage: - API layer (tests/e2e/api/): unauthenticated access, admin endpoints, peer endpoints, access control enforcement — 24 tests - Playwright UI (tests/e2e/ui/): login flows, admin navigation, peer dashboard/services, role-based ACL, password change — 60+ tests - WireGuard connectivity (tests/e2e/wg/): tunnel up/down, DNS resolution through VPN, service ACL enforcement via iptables, full-tunnel routing Shared helpers: PicAPIClient, WGInterface, playwright_login, cleanup. Makefile targets: test-e2e-api, test-e2e-ui, test-e2e-wg, test-e2e. Adds scripts/reset_admin_password.py for test bootstrap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Peer access-control tests (scenarios 14 & 15).
|
||||
|
||||
PrivateRoute.jsx (confirmed):
|
||||
- Unauthenticated users → <Navigate to="/login" />
|
||||
- Authenticated user with wrong role → <Navigate to="/" />
|
||||
|
||||
A peer (role='peer') visiting an admin-only route must be redirected to '/'.
|
||||
A peer must NOT see admin sidebar links (Peers, Settings, WireGuard, etc.).
|
||||
"""
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.ui
|
||||
|
||||
# All routes that require role='admin' (from App.jsx Routes).
|
||||
ADMIN_ONLY_ROUTES = [
|
||||
'/peers',
|
||||
'/network',
|
||||
'/wireguard',
|
||||
'/email',
|
||||
'/calendar',
|
||||
'/files',
|
||||
'/routing',
|
||||
'/vault',
|
||||
'/containers',
|
||||
'/cell-network',
|
||||
'/logs',
|
||||
'/settings',
|
||||
]
|
||||
|
||||
# Admin-only sidebar link names (from App.jsx adminNavigation).
|
||||
ADMIN_ONLY_NAV_LINKS = [
|
||||
'Peers',
|
||||
'Network Services',
|
||||
'WireGuard',
|
||||
'Email',
|
||||
'Calendar',
|
||||
'Files',
|
||||
'Routing',
|
||||
'Vault',
|
||||
'Containers',
|
||||
'Cell Network',
|
||||
'Logs',
|
||||
'Settings',
|
||||
]
|
||||
|
||||
|
||||
# ── Scenario 14: peer redirected from admin routes ───────────────────────────
|
||||
|
||||
@pytest.mark.parametrize('admin_route', ADMIN_ONLY_ROUTES)
|
||||
def test_peer_redirected_from_admin_route(peer_page, webui_base, admin_route):
|
||||
"""
|
||||
A peer navigating to an admin-only route must NOT land on that route.
|
||||
PrivateRoute redirects them to '/' instead.
|
||||
"""
|
||||
page, _ = peer_page
|
||||
page.goto(f"{webui_base}{admin_route}")
|
||||
page.wait_for_load_state('networkidle')
|
||||
|
||||
current_path = page.url.replace(webui_base, '')
|
||||
assert current_path.rstrip('/') not in [admin_route.rstrip('/')], (
|
||||
f"Peer was allowed to reach admin-only route '{admin_route}'. "
|
||||
f"Expected redirect to '/'. Got: {page.url}"
|
||||
)
|
||||
# Must not have been sent to /login either — peer IS authenticated.
|
||||
assert '/login' not in page.url, (
|
||||
f"Peer was unexpectedly redirected to /login from '{admin_route}'. "
|
||||
"PrivateRoute should redirect role-mismatches to '/', not /login."
|
||||
)
|
||||
|
||||
|
||||
# ── Scenario 15: peer sidebar lacks admin links ──────────────────────────────
|
||||
|
||||
def test_peer_nav_does_not_show_admin_only_links(peer_page, webui_base):
|
||||
"""
|
||||
The peer sidebar (peerNavigation in App.jsx) only contains Dashboard,
|
||||
My Services, and Account. Admin-only links must be absent.
|
||||
"""
|
||||
page, _ = peer_page
|
||||
# Navigate to root so the sidebar is fully rendered.
|
||||
page.goto(f"{webui_base}/")
|
||||
page.wait_for_load_state('networkidle')
|
||||
|
||||
for link_name in ADMIN_ONLY_NAV_LINKS:
|
||||
assert not page.get_by_role('link', name=link_name).is_visible(), (
|
||||
f"Admin-only sidebar link '{link_name}' should NOT be visible to a peer"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_nav_shows_allowed_links(peer_page, webui_base):
|
||||
"""
|
||||
The peer sidebar must contain exactly the three peer navigation items:
|
||||
Dashboard, My Services, Account.
|
||||
"""
|
||||
page, _ = peer_page
|
||||
page.goto(f"{webui_base}/")
|
||||
page.wait_for_load_state('networkidle')
|
||||
|
||||
for link_name in ('Dashboard', 'My Services', 'Account'):
|
||||
assert page.get_by_role('link', name=link_name).is_visible(), (
|
||||
f"Peer sidebar should show link '{link_name}'"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_my_services_is_accessible(peer_page, webui_base):
|
||||
"""
|
||||
/my-services is restricted to role='peer' (requireRole="peer" in App.jsx).
|
||||
A logged-in peer must be able to reach it.
|
||||
"""
|
||||
page, _ = peer_page
|
||||
page.goto(f"{webui_base}/my-services")
|
||||
page.wait_for_load_state('networkidle')
|
||||
assert '/login' not in page.url
|
||||
assert '/my-services' in page.url
|
||||
Reference in New Issue
Block a user