Files
pic/tests/e2e/api/test_admin_endpoints.py
roof 0d32038150 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>
2026-04-25 16:41:13 -04:00

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}"
)