Files
pic/tests/e2e/api/test_peer_endpoints.py
T
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

122 lines
4.3 KiB
Python

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