Files
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

75 lines
2.5 KiB
Python

"""
Scenario 18: Unauthenticated requests are blocked.
All protected API endpoints must return 401 when no session cookie is present.
The health endpoint and the login endpoint itself must remain publicly accessible.
"""
import requests
import pytest
@pytest.fixture(scope='module')
def anon(api_base):
"""Plain unauthenticated requests.Session — no cookies, no auth headers."""
s = requests.Session()
s.headers['Content-Type'] = 'application/json'
return s
# ---------------------------------------------------------------------------
# Protected endpoints must return 401 for unauthenticated callers
# ---------------------------------------------------------------------------
def test_config_requires_auth(anon, api_base):
r = anon.get(f"{api_base}/api/config")
assert r.status_code == 401, (
f"GET /api/config should require auth, got {r.status_code}"
)
def test_peers_requires_auth(anon, api_base):
r = anon.get(f"{api_base}/api/peers")
assert r.status_code == 401, (
f"GET /api/peers should require auth, got {r.status_code}"
)
def test_wireguard_requires_auth(anon, api_base):
r = anon.get(f"{api_base}/api/wireguard/status")
assert r.status_code == 401, (
f"GET /api/wireguard/status should require auth, got {r.status_code}"
)
def test_auth_me_unauthenticated(anon, api_base):
r = anon.get(f"{api_base}/api/auth/me")
assert r.status_code == 401, (
f"GET /api/auth/me without session should return 401, got {r.status_code}"
)
# ---------------------------------------------------------------------------
# Public endpoints must remain reachable without auth
# ---------------------------------------------------------------------------
def test_auth_login_is_public(anon, api_base):
"""POST /api/auth/login is reachable without a session.
Wrong credentials → 401, but NOT 403 (which would mean the endpoint
itself is blocked by the auth hook rather than the credential check).
"""
r = anon.post(f"{api_base}/api/auth/login",
json={'username': 'nobody', 'password': 'badpassword'})
assert r.status_code == 401, (
f"POST /api/auth/login with wrong creds should return 401 (not 403), "
f"got {r.status_code}"
)
def test_health_is_public(anon, api_base):
"""GET /health must return 200 without any session (used by Docker + load-balancers)."""
r = anon.get(f"{api_base}/health")
assert r.status_code == 200, (
f"GET /health should be publicly accessible, got {r.status_code}"
)