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>
106 lines
3.0 KiB
Python
106 lines
3.0 KiB
Python
import os
|
|
import pytest
|
|
import tempfile
|
|
import secrets
|
|
from helpers.wg_runner import WGInterface, build_wg_config, cleanup_stale_e2e_interfaces
|
|
|
|
|
|
@pytest.fixture(scope='session', autouse=True)
|
|
def cleanup_stale_wg_interfaces():
|
|
cleanup_stale_e2e_interfaces()
|
|
yield
|
|
cleanup_stale_e2e_interfaces()
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def wg_server_info(admin_client, pic_host):
|
|
"""Get server public key and endpoint from the running API."""
|
|
r = admin_client.get('/api/wireguard/status')
|
|
data = r.json()
|
|
# status might be nested — check common shapes
|
|
server_pubkey = (
|
|
data.get('public_key') or
|
|
data.get('server_public_key') or
|
|
data.get('status', {}).get('public_key', '')
|
|
)
|
|
port = data.get('port') or data.get('listen_port') or 51820
|
|
return {
|
|
'public_key': server_pubkey,
|
|
'endpoint': pic_host,
|
|
'port': int(port),
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def connected_peer(make_peer, wg_server_info, tmp_path):
|
|
"""
|
|
Creates a peer, builds its WireGuard config, brings the tunnel up, yields,
|
|
then tears everything down.
|
|
|
|
Requires: sudo wg-quick available on the test runner.
|
|
"""
|
|
peer = make_peer('e2etest-wg-basic', service_access=['calendar', 'files', 'mail', 'webdav'])
|
|
|
|
iface_name = f"pic-e2e-{secrets.token_hex(3)}"
|
|
conf_path = str(tmp_path / f"{iface_name}.conf")
|
|
|
|
config_text = build_wg_config(
|
|
private_key=peer['private_key'],
|
|
peer_ip=peer['ip'],
|
|
server_pubkey=wg_server_info['public_key'],
|
|
server_endpoint=wg_server_info['endpoint'],
|
|
server_port=wg_server_info['port'],
|
|
allowed_ips='10.0.0.0/24',
|
|
)
|
|
|
|
# Write config with restricted permissions
|
|
with open(conf_path, 'w') as f:
|
|
f.write(config_text)
|
|
os.chmod(conf_path, 0o600)
|
|
|
|
iface = WGInterface(conf_path, iface_name)
|
|
try:
|
|
iface.bring_up()
|
|
peer['iface'] = iface
|
|
peer['conf_path'] = conf_path
|
|
yield peer
|
|
finally:
|
|
iface.bring_down()
|
|
try:
|
|
os.unlink(conf_path)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def full_tunnel_peer(make_peer, wg_server_info, tmp_path):
|
|
"""Like connected_peer but with AllowedIPs=0.0.0.0/0 (full tunnel)."""
|
|
peer = make_peer('e2etest-wg-fulltunnel')
|
|
iface_name = f"pic-e2e-{secrets.token_hex(3)}"
|
|
conf_path = str(tmp_path / f"{iface_name}.conf")
|
|
|
|
config_text = build_wg_config(
|
|
private_key=peer['private_key'],
|
|
peer_ip=peer['ip'],
|
|
server_pubkey=wg_server_info['public_key'],
|
|
server_endpoint=wg_server_info['endpoint'],
|
|
server_port=wg_server_info['port'],
|
|
allowed_ips='0.0.0.0/0',
|
|
)
|
|
with open(conf_path, 'w') as f:
|
|
f.write(config_text)
|
|
os.chmod(conf_path, 0o600)
|
|
|
|
iface = WGInterface(conf_path, iface_name)
|
|
try:
|
|
iface.bring_up()
|
|
peer['iface'] = iface
|
|
peer['conf_path'] = conf_path
|
|
yield peer
|
|
finally:
|
|
iface.bring_down()
|
|
try:
|
|
os.unlink(conf_path)
|
|
except Exception:
|
|
pass
|