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>
122 lines
4.3 KiB
Python
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}"
|
|
)
|