""" Top-level conftest for PIC E2E tests. Configure with environment variables (or a .env file in this directory): PIC_HOST API / WebUI host (default: localhost) PIC_API_PORT API port (default: 3000) PIC_WEBUI_PORT WebUI port (default: 8081) PIC_ADMIN_USER Admin username (default: admin) PIC_ADMIN_PASS Admin password (or read from data/api/.admin_initial_password) """ import os import sys import pytest # Allow helpers to be imported without installing the package sys.path.insert(0, os.path.dirname(__file__)) from helpers.admin_password import resolve_admin_password from helpers.api_client import PicAPIClient from helpers.cleanup import delete_e2e_peers # --------------------------------------------------------------------------- # pytest hooks # --------------------------------------------------------------------------- def pytest_configure(config): from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(__file__), '.env')) def pytest_sessionstart(session): # Verify PIC API is reachable before running any tests import requests, os host = os.environ.get('PIC_HOST', 'localhost') port = os.environ.get('PIC_API_PORT', '3000') try: r = requests.get(f"http://{host}:{port}/health", timeout=5) if r.status_code != 200: raise RuntimeError(f"PIC API unhealthy: {r.status_code}") except Exception as e: raise RuntimeError(f"PIC API not reachable at {host}:{port}. Run 'make start' first. Error: {e}") # --------------------------------------------------------------------------- # Session-scoped infrastructure fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope='session') def pic_host(): return os.environ.get('PIC_HOST', 'localhost') @pytest.fixture(scope='session') def api_port(): return int(os.environ.get('PIC_API_PORT', '3000')) @pytest.fixture(scope='session') def webui_port(): return int(os.environ.get('PIC_WEBUI_PORT', '8081')) @pytest.fixture(scope='session') def api_base(pic_host, api_port): return f"http://{pic_host}:{api_port}" @pytest.fixture(scope='session') def webui_base(pic_host, webui_port): return f"http://{pic_host}:{webui_port}" @pytest.fixture(scope='session') def admin_user(): return os.environ.get('PIC_ADMIN_USER', 'admin') @pytest.fixture(scope='session') def admin_password(): return resolve_admin_password() @pytest.fixture(scope='session') def admin_client(api_base, admin_user, admin_password): """Authenticated PicAPIClient logged in as admin — shared for the whole session.""" client = PicAPIClient(api_base) client.login(admin_user, admin_password) return client # --------------------------------------------------------------------------- # Peer cleanup — runs before and after the entire session # --------------------------------------------------------------------------- @pytest.fixture(scope='session', autouse=True) def clean_test_peers(admin_client): """Delete any e2etest-* peers left over from previous runs (and after this run).""" delete_e2e_peers(admin_client) yield delete_e2e_peers(admin_client) # --------------------------------------------------------------------------- # Peer factory — function-scoped # --------------------------------------------------------------------------- @pytest.fixture def make_peer(request, admin_client): """ Factory fixture that creates a WireGuard peer via the API. Usage:: def test_something(make_peer): peer = make_peer('e2etest-foo') # peer = {name, password, public_key, private_key, ip} The peer is deleted automatically after the test. All names MUST start with 'e2etest-'. """ created = [] def _factory(name: str, password: str = 'TestPass123!', service_access=None): assert name.startswith('e2etest-'), ( f"Test peer name '{name}' must start with 'e2etest-' for safe cleanup" ) # Default: grant access to all services if service_access is None: service_access = ['calendar', 'files', 'mail', 'webdav'] # 1. Generate WireGuard key pair r = admin_client.post('/api/wireguard/keys/peer', json={'name': name}) assert r.status_code == 200, ( f"Key generation failed for '{name}': {r.status_code} {r.text}" ) keys = r.json() assert 'public_key' in keys and 'private_key' in keys, ( f"Key response missing keys: {keys}" ) # 2. Create peer payload = { 'name': name, 'public_key': keys['public_key'], 'password': password, 'service_access': service_access, } r = admin_client.post('/api/peers', json=payload) assert r.status_code == 201, ( f"Peer creation failed for '{name}': {r.status_code} {r.text}" ) data = r.json() peer_info = { 'name': name, 'password': password, 'public_key': keys['public_key'], 'private_key': keys['private_key'], 'ip': data.get('ip', ''), } created.append(name) def _cleanup(): admin_client.delete(f'/api/peers/{name}') request.addfinalizer(_cleanup) return peer_info return _factory # --------------------------------------------------------------------------- # Convenience peer_client fixture # --------------------------------------------------------------------------- @pytest.fixture def peer_client(make_peer, api_base): """ A PicAPIClient already logged in as a freshly created peer. The underlying peer is named 'e2etest-peer-client' and is deleted after the test via make_peer's finalizer. """ peer = make_peer('e2etest-peer-client') client = PicAPIClient(api_base) client.login(peer['name'], peer['password']) return client