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,136 @@
|
||||
"""
|
||||
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}"
|
||||
)
|
||||
Reference in New Issue
Block a user