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>
116 lines
3.9 KiB
Python
116 lines
3.9 KiB
Python
"""
|
|
Admin backup / restore tests.
|
|
|
|
Scenario 10: create a backup and verify it appears in the list.
|
|
|
|
These tests use the API directly for the heavy lifting — the backup list
|
|
UI just renders what the API returns, so API-level assertions are sufficient
|
|
and significantly more stable than chasing DOM selectors.
|
|
"""
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.ui
|
|
|
|
|
|
def test_create_backup_returns_backup_id(admin_client):
|
|
"""POST /api/config/backup succeeds and returns a backup identifier."""
|
|
r = admin_client.post('/api/config/backup')
|
|
assert r.status_code == 200, (
|
|
f"Backup creation failed: {r.status_code} {r.text}"
|
|
)
|
|
data = r.json()
|
|
backup_id = data.get('backup_id') or data.get('id') or data.get('filename')
|
|
assert backup_id, f"Response did not contain a backup ID: {data}"
|
|
|
|
|
|
def test_create_backup_appears_in_list(admin_client):
|
|
"""A freshly created backup must be retrievable from GET /api/config/backups."""
|
|
# Create
|
|
r = admin_client.post('/api/config/backup')
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
backup_id = data.get('backup_id') or data.get('id') or data.get('filename')
|
|
assert backup_id, f"No backup ID in response: {data}"
|
|
|
|
# List
|
|
r2 = admin_client.get('/api/config/backups')
|
|
assert r2.status_code == 200, (
|
|
f"GET /api/config/backups failed: {r2.status_code} {r2.text}"
|
|
)
|
|
backups = r2.json()
|
|
assert isinstance(backups, list), f"Expected list, got: {type(backups)}"
|
|
|
|
# Accept either a flat list of ID strings or a list of dicts with id/backup_id/filename
|
|
ids = []
|
|
for b in backups:
|
|
if isinstance(b, str):
|
|
ids.append(b)
|
|
elif isinstance(b, dict):
|
|
ids.append(b.get('backup_id') or b.get('id') or b.get('filename') or '')
|
|
|
|
assert backup_id in ids, (
|
|
f"Backup '{backup_id}' not found in backup list: {ids}"
|
|
)
|
|
|
|
|
|
def test_backup_list_not_empty_after_create(admin_client):
|
|
"""After at least one backup, the backup list must be non-empty."""
|
|
admin_client.post('/api/config/backup')
|
|
r = admin_client.get('/api/config/backups')
|
|
assert r.status_code == 200
|
|
assert len(r.json()) > 0
|
|
|
|
|
|
def test_backup_download_returns_content(admin_client):
|
|
"""
|
|
Downloading a backup archive should return HTTP 200 with non-empty content.
|
|
|
|
Tries common download URL patterns; skips cleanly if none succeed.
|
|
"""
|
|
r = admin_client.post('/api/config/backup')
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
backup_id = data.get('backup_id') or data.get('id') or data.get('filename')
|
|
assert backup_id
|
|
|
|
# Try multiple plausible URL shapes
|
|
candidate_paths = [
|
|
f'/api/config/backups/{backup_id}/download',
|
|
f'/api/config/backup/{backup_id}/download',
|
|
f'/api/config/backups/{backup_id}',
|
|
]
|
|
dl = None
|
|
for path in candidate_paths:
|
|
resp = admin_client.get(path)
|
|
if resp.status_code == 200:
|
|
dl = resp
|
|
break
|
|
|
|
if dl is None:
|
|
pytest.skip(
|
|
f"No download endpoint responded 200 for backup '{backup_id}'. "
|
|
"Tried: " + ', '.join(candidate_paths)
|
|
)
|
|
|
|
assert len(dl.content) > 0, "Backup download returned empty body"
|
|
|
|
|
|
def test_backup_page_renders_in_browser(admin_page, webui_base):
|
|
"""
|
|
The Settings page (which hosts the backup UI) renders without redirecting
|
|
to /login and shows some backup-related text.
|
|
"""
|
|
page = admin_page
|
|
page.goto(f"{webui_base}/settings")
|
|
page.wait_for_load_state('networkidle')
|
|
assert '/login' not in page.url
|
|
# Settings.jsx imports Archive icon and renders backup section.
|
|
# Look for the word "Backup" anywhere on the page.
|
|
try:
|
|
page.wait_for_selector('text=Backup', timeout=5000)
|
|
except Exception:
|
|
pytest.xfail(
|
|
"Backup section text not found on /settings — "
|
|
"check Settings.jsx for the backup section heading"
|
|
)
|