feat: add integration test suite (66 tests covering live API + services + UI)
Tests cover health, config, all 12 containers, WireGuard, DNS/DHCP/NTP, services status, peer CRUD with iptables rule verification, service_access enforcement (full/restricted/no-access), and WebUI smoke tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Shared fixtures for live integration tests.
|
||||
|
||||
Configure with environment variables:
|
||||
PIC_HOST API host (default: localhost)
|
||||
PIC_API_PORT API port (default: 3000)
|
||||
PIC_WEBUI_PORT WebUI port (default: 80)
|
||||
PIC_WG_CONTAINER WireGuard container name (default: cell-wireguard)
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
PIC_HOST = os.environ.get('PIC_HOST', 'localhost')
|
||||
API_PORT = int(os.environ.get('PIC_API_PORT', '3000'))
|
||||
WEBUI_PORT = int(os.environ.get('PIC_WEBUI_PORT', '80'))
|
||||
WG_CONTAINER = os.environ.get('PIC_WG_CONTAINER', 'cell-wireguard')
|
||||
|
||||
API_BASE = f"http://{PIC_HOST}:{API_PORT}"
|
||||
WEBUI_BASE = f"http://{PIC_HOST}:{WEBUI_PORT}"
|
||||
|
||||
TEST_PEERS = (
|
||||
'integration-test-full',
|
||||
'integration-test-restricted',
|
||||
'integration-test-none',
|
||||
'bad-svc-peer', # guard against validation-test leak
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def api():
|
||||
s = requests.Session()
|
||||
s.headers['Content-Type'] = 'application/json'
|
||||
return s
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def api_base():
|
||||
return API_BASE
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def webui_base():
|
||||
return WEBUI_BASE
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def cleanup_test_peers(api):
|
||||
"""Delete any leftover test peers before and after the entire session."""
|
||||
for name in TEST_PEERS:
|
||||
api.delete(f"{API_BASE}/api/peers/{name}")
|
||||
yield
|
||||
for name in TEST_PEERS:
|
||||
api.delete(f"{API_BASE}/api/peers/{name}")
|
||||
|
||||
|
||||
def iptables_forward() -> str:
|
||||
"""Return iptables-save output from the WireGuard container."""
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', WG_CONTAINER, 'iptables-save'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def peer_rules(peer_ip: str) -> list[str]:
|
||||
"""Return FORWARD rule lines for a specific peer IP."""
|
||||
comment = f'pic-peer-{peer_ip.replace(".", "-")}'
|
||||
return [line for line in iptables_forward().splitlines() if comment in line]
|
||||
|
||||
|
||||
def get_live_service_vips() -> dict:
|
||||
"""
|
||||
Read SERVICE_IPS directly from the running API container.
|
||||
More reliable than the config API since SERVICE_IPS may not match ip_range
|
||||
when the container was built before an ip_range change.
|
||||
"""
|
||||
import json
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'cell-api', 'python3', '-c',
|
||||
'import sys; sys.path.insert(0,"/app/api");'
|
||||
' from firewall_manager import SERVICE_IPS; import json; print(json.dumps(SERVICE_IPS))'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return json.loads(result.stdout)
|
||||
# Fallback: derive from config API
|
||||
cfg = requests.get(f"{API_BASE}/api/config").json()
|
||||
sips = cfg.get('service_ips', {})
|
||||
return {
|
||||
'calendar': sips.get('vip_calendar', ''),
|
||||
'files': sips.get('vip_files', ''),
|
||||
'mail': sips.get('vip_mail', ''),
|
||||
'webdav': sips.get('vip_webdav', ''),
|
||||
}
|
||||
Reference in New Issue
Block a user