32272420cb
- test_peer_dashboard_services.py (63 tests): unit tests for all API fixes * peer_dashboard field names (name/transfer_rx/transfer_tx vs old stale names) * peer_dashboard service_urls dict with correct domain-keyed URLs * peer_services email structure (nested smtp/imap, address not username) * peer_services files key (not webdav), caldav URL (calendar.dev not radicale.dev:5232) * peer_services wireguard DNS (not 10.0.0.1), config text with DNS line * DNS zone records (api/webui → Caddy, VIPs for calendar/files/mail/webdav) * Caddyfile generation (all service blocks including webui.dev) * Access control (401 anon, 403 admin on peer-only routes, 404 missing peer) - e2e/api/test_peer_endpoints.py: fix stale field assertions, add structure checks - e2e/wg/test_wg_domain_access.py: E2E WG tests for DNS resolution via VPN tunnel * All *.dev domains resolve to correct IPs via CoreDNS * api.dev/webui.dev must resolve to Caddy, not container direct IPs * CoreDNS reachability through VPN tunnel * Peer config DNS field correctness - e2e/ui/test_peer_dashboard.py: UI checks for service icon links, CalDAV URL, email Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
7.0 KiB
Python
183 lines
7.0 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 (name, online, transfer_rx, transfer_tx, service_urls)
|
|
- Services response shape (wireguard, email, caldav, files 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 ('name', 'online', 'transfer_rx', 'transfer_tx', 'allowed_ips', 'service_urls') if f not in data]
|
|
assert not missing, (
|
|
f"Dashboard response missing fields {missing}. Got keys: {list(data.keys())}"
|
|
)
|
|
|
|
|
|
def test_peer_dashboard_no_stale_field_names(peer_client):
|
|
"""Verify renamed fields are gone — old names cause silent UI blanks."""
|
|
r = peer_client.get('/api/peer/dashboard')
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
stale = [f for f in ('peer_name', 'rx_bytes', 'tx_bytes') if f in data]
|
|
assert not stale, (
|
|
f"Dashboard response still has stale fields {stale} — "
|
|
"PeerDashboard.jsx reads name/transfer_rx/transfer_tx"
|
|
)
|
|
|
|
|
|
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', 'files') if k not in data]
|
|
assert not missing, (
|
|
f"Services response missing sections {missing}. Got keys: {list(data.keys())}"
|
|
)
|
|
|
|
|
|
def test_peer_services_no_stale_keys(peer_client):
|
|
"""Verify renamed keys are gone — old names cause silent UI blanks."""
|
|
r = peer_client.get('/api/peer/services')
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert 'webdav' not in data, (
|
|
"'webdav' still present at top level — MyServices.jsx reads 'files'"
|
|
)
|
|
|
|
|
|
def test_peer_services_email_structure(peer_client):
|
|
"""Email section must use nested smtp/imap objects and email.address."""
|
|
r = peer_client.get('/api/peer/services')
|
|
assert r.status_code == 200
|
|
email = r.json().get('email', {})
|
|
assert 'address' in email, f"email.address missing; email keys: {list(email)}"
|
|
assert 'smtp' in email and isinstance(email['smtp'], dict), \
|
|
f"email.smtp must be a dict; got: {email.get('smtp')}"
|
|
assert 'imap' in email and isinstance(email['imap'], dict), \
|
|
f"email.imap must be a dict; got: {email.get('imap')}"
|
|
assert 'host' in email['smtp'], "email.smtp.host missing"
|
|
assert 'host' in email['imap'], "email.imap.host missing"
|
|
assert 'imap_host' not in email, "'imap_host' still flat — should be email.imap.host"
|
|
assert 'smtp_host' not in email, "'smtp_host' still flat — should be email.smtp.host"
|
|
|
|
|
|
def test_peer_services_caldav_url_uses_calendar_domain(peer_client):
|
|
"""CalDAV URL must be calendar.dev, not radicale.dev:5232."""
|
|
r = peer_client.get('/api/peer/services')
|
|
assert r.status_code == 200
|
|
url = r.json().get('caldav', {}).get('url', '')
|
|
assert 'radicale' not in url, \
|
|
f"CalDAV URL must not contain 'radicale' — no radicale.dev DNS record; got: {url}"
|
|
assert ':5232' not in url, \
|
|
f"CalDAV URL exposes port 5232 — use Caddy-proxied URL; got: {url}"
|
|
|
|
|
|
def test_peer_services_wireguard_dns_not_vpn_gateway(peer_client):
|
|
"""WireGuard DNS must be the CoreDNS IP, not the VPN gateway 10.0.0.1."""
|
|
r = peer_client.get('/api/peer/services')
|
|
assert r.status_code == 200
|
|
dns = r.json().get('wireguard', {}).get('dns', '')
|
|
assert dns != '10.0.0.1', (
|
|
"wireguard.dns is 10.0.0.1 (WireGuard VPN gateway) — "
|
|
"DNS queries to 10.0.0.1 fail because the VPN server doesn't run a DNS resolver; "
|
|
"must be the CoreDNS container IP"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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}"
|
|
)
|