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>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
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}"
|
||||
)
|
||||
Reference in New Issue
Block a user