0d32038150
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>
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
"""
|
|
Scenarios 19, 22, 23, 24: Admin role access and peer management.
|
|
|
|
Tests cover:
|
|
- Admin can read configuration and list peers
|
|
- Admin is blocked from peer-only routes (/api/peer/*)
|
|
- Peer creation validation (missing/weak password)
|
|
- Full create-and-delete peer lifecycle
|
|
- Admin can list auth users
|
|
"""
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Read access
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_can_get_config(admin_client):
|
|
r = admin_client.get('/api/config')
|
|
assert r.status_code == 200, (
|
|
f"Admin should be able to GET /api/config, got {r.status_code}"
|
|
)
|
|
data = r.json()
|
|
# Config must contain at least one well-known top-level key
|
|
assert 'cell_name' in data or 'service_configs' in data or 'ip_range' in data, (
|
|
f"Config response missing expected keys: {list(data.keys())}"
|
|
)
|
|
|
|
|
|
def test_admin_can_list_peers(admin_client):
|
|
r = admin_client.get('/api/peers')
|
|
assert r.status_code == 200, (
|
|
f"Admin should be able to GET /api/peers, got {r.status_code}"
|
|
)
|
|
assert isinstance(r.json(), list), (
|
|
f"GET /api/peers should return a list, got {type(r.json())}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Peer-only routes must be blocked for admin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_cannot_access_peer_dashboard(admin_client):
|
|
r = admin_client.get('/api/peer/dashboard')
|
|
assert r.status_code == 403, (
|
|
f"Admin should be blocked from /api/peer/dashboard with 403, got {r.status_code}"
|
|
)
|
|
|
|
|
|
def test_admin_cannot_access_peer_services(admin_client):
|
|
r = admin_client.get('/api/peer/services')
|
|
assert r.status_code == 403, (
|
|
f"Admin should be blocked from /api/peer/services with 403, got {r.status_code}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Peer creation validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_create_peer_missing_password(admin_client):
|
|
"""POST /api/peers with name + public_key but no password must return 400."""
|
|
# Use a fixed throwaway key; it doesn't need to be a real WireGuard key for
|
|
# validation tests — the password check should happen before key verification.
|
|
r = admin_client.post('/api/peers', json={
|
|
'name': 'e2etest-no-password',
|
|
'public_key': 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
})
|
|
assert r.status_code == 400, (
|
|
f"Creating peer without password should return 400, got {r.status_code}"
|
|
)
|
|
|
|
|
|
def test_create_peer_short_password(admin_client):
|
|
"""POST /api/peers with a 5-character password must return 400."""
|
|
r = admin_client.post('/api/peers', json={
|
|
'name': 'e2etest-short-pass',
|
|
'public_key': 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
'password': 'Ab1!x',
|
|
})
|
|
assert r.status_code == 400, (
|
|
f"Creating peer with 5-char password should return 400, got {r.status_code}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Full create and delete lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_create_and_delete_peer(admin_client, make_peer):
|
|
"""Create a peer, verify it appears in the list, delete it, verify it's gone."""
|
|
peer = make_peer('e2etest-lifecycle')
|
|
|
|
# Peer must appear in the list
|
|
r = admin_client.get('/api/peers')
|
|
assert r.status_code == 200
|
|
peers = r.json()
|
|
names = [p.get('peer') or p.get('name', '') for p in peers]
|
|
assert 'e2etest-lifecycle' in names, (
|
|
f"Newly created peer 'e2etest-lifecycle' not found in /api/peers: {names}"
|
|
)
|
|
|
|
# Delete the peer manually (make_peer's finalizer will also attempt deletion)
|
|
r = admin_client.delete('/api/peers/e2etest-lifecycle')
|
|
assert r.status_code == 200, (
|
|
f"DELETE /api/peers/e2etest-lifecycle should return 200, got {r.status_code}"
|
|
)
|
|
|
|
# Verify it's gone
|
|
r = admin_client.get('/api/peers')
|
|
assert r.status_code == 200
|
|
peers_after = r.json()
|
|
names_after = [p.get('peer') or p.get('name', '') for p in peers_after]
|
|
assert 'e2etest-lifecycle' not in names_after, (
|
|
f"Deleted peer 'e2etest-lifecycle' still appears in /api/peers: {names_after}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auth user management
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_can_list_auth_users(admin_client):
|
|
r = admin_client.get('/api/auth/users')
|
|
assert r.status_code == 200, (
|
|
f"Admin should be able to GET /api/auth/users, got {r.status_code}"
|
|
)
|
|
users = r.json()
|
|
assert isinstance(users, list), (
|
|
f"GET /api/auth/users should return a list, got {type(users)}"
|
|
)
|
|
usernames = [u.get('username') for u in users]
|
|
assert 'admin' in usernames, (
|
|
f"'admin' not found in user list: {usernames}"
|
|
)
|