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