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,121 @@
|
||||
"""
|
||||
Scenarios 20, 21: Peer role access scoping.
|
||||
|
||||
Tests cover:
|
||||
- Peer is blocked from admin-only routes (config, wireguard, peer list)
|
||||
- Peer can access /api/peer/dashboard and /api/peer/services
|
||||
- Dashboard response shape (peer_name, online, rx_bytes, tx_bytes, allowed_ips)
|
||||
- Services response shape (wireguard, email, caldav, webdav sections)
|
||||
- Peer can change their own password and use the new credential
|
||||
- Peer cannot call admin/reset-password
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from helpers.api_client import PicAPIClient
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Admin-only routes must be blocked for peer role
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_peer_cannot_access_config(peer_client):
|
||||
r = peer_client.get('/api/config')
|
||||
assert r.status_code == 403, (
|
||||
f"Peer should be blocked from /api/config with 403, got {r.status_code}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_cannot_access_wireguard_settings(peer_client):
|
||||
r = peer_client.get('/api/wireguard/status')
|
||||
assert r.status_code == 403, (
|
||||
f"Peer should be blocked from /api/wireguard/status with 403, got {r.status_code}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_cannot_list_peers(peer_client):
|
||||
r = peer_client.get('/api/peers')
|
||||
assert r.status_code == 403, (
|
||||
f"Peer should be blocked from GET /api/peers with 403, got {r.status_code}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Peer-accessible routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_peer_can_access_own_dashboard(peer_client):
|
||||
r = peer_client.get('/api/peer/dashboard')
|
||||
assert r.status_code == 200, (
|
||||
f"Peer should be able to GET /api/peer/dashboard, got {r.status_code}: {r.text}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_dashboard_has_expected_fields(peer_client):
|
||||
r = peer_client.get('/api/peer/dashboard')
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
missing = [f for f in ('peer_name', 'online', 'rx_bytes', 'tx_bytes', 'allowed_ips') if f not in data]
|
||||
assert not missing, (
|
||||
f"Dashboard response missing fields {missing}. Got keys: {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_can_access_own_services(peer_client):
|
||||
r = peer_client.get('/api/peer/services')
|
||||
assert r.status_code == 200, (
|
||||
f"Peer should be able to GET /api/peer/services, got {r.status_code}: {r.text}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_services_has_expected_sections(peer_client):
|
||||
r = peer_client.get('/api/peer/services')
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
missing = [k for k in ('wireguard', 'email', 'caldav', 'webdav') if k not in data]
|
||||
assert not missing, (
|
||||
f"Services response missing sections {missing}. Got keys: {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth management — scoping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_peer_cannot_access_auth_users(peer_client):
|
||||
r = peer_client.get('/api/auth/users')
|
||||
assert r.status_code == 403, (
|
||||
f"Peer should be blocked from GET /api/auth/users with 403, got {r.status_code}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_cannot_reset_other_password(peer_client):
|
||||
r = peer_client.post('/api/auth/admin/reset-password',
|
||||
json={'username': 'admin', 'new_password': 'HackedPass1!'})
|
||||
assert r.status_code == 403, (
|
||||
f"Peer should be blocked from admin/reset-password with 403, got {r.status_code}"
|
||||
)
|
||||
|
||||
|
||||
def test_peer_can_change_own_password(make_peer, api_base):
|
||||
"""
|
||||
A peer can change their own password via POST /api/auth/change-password.
|
||||
After the change the new password must work for login.
|
||||
"""
|
||||
peer = make_peer('e2etest-change-pass', password='OldPass123!')
|
||||
|
||||
client = PicAPIClient(api_base)
|
||||
client.login(peer['name'], 'OldPass123!')
|
||||
|
||||
r = client.post('/api/auth/change-password',
|
||||
json={'old_password': 'OldPass123!', 'new_password': 'NewPass456!'})
|
||||
assert r.status_code == 200, (
|
||||
f"change-password should return 200, got {r.status_code}: {r.text}"
|
||||
)
|
||||
|
||||
# Verify new password works
|
||||
new_client = PicAPIClient(api_base)
|
||||
new_client.login(peer['name'], 'NewPass456!')
|
||||
me = new_client.me()
|
||||
assert me.get('username') == peer['name'], (
|
||||
f"Login with new password failed — me() returned: {me}"
|
||||
)
|
||||
Reference in New Issue
Block a user