feat: comprehensive test suite + SAML auth fixes + mock SAML IdP
Tests (198 unit + 70 e2e = 268 total): - Add test_api_deps.py: Bearer token auth, get_current_api_user, require_admin - Add test_wireguard_extended.py: ensure_interface, set_private_key, set_listen_port - Add test_firewall_extended.py: _nft/_nft_batch errors, jump rules, policies - Add test_mfa_login.py: MFA redirect, TOTP verify, invalid code, cancel - Add test_magic_link_page.py: page render, submit, empty email, back to login - Add test_admin_devices.py: list, filter, create, edit, delete, config dialog - Add test_admin_rules.py: list, create, edit, delete (all DB-verified) - Add test_admin_settings.py: defaults, security, OIDC/SAML providers - Add test_saml_login.py: button visible, redirect, metadata, full login flow Bug fixes: - Fix SAML callback to use /auth/complete bridge (same fix as OIDC) - Fix missing get_settings import in admin settings page - Add SAML provider buttons to login page - Make SAML strict mode configurable per-provider Infrastructure: - Add mock SimpleSAMLphp IdP to compose.yml with SP config - Add mock-saml service to CI workflows (release + dev)
This commit is contained in:
parent
25cff5e4d9
commit
06b5a3dc12
18 changed files with 1768 additions and 47 deletions
239
tests/e2e/test_admin_devices.py
Normal file
239
tests/e2e/test_admin_devices.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""E2E tests for admin device management page."""
|
||||
|
||||
import pytest_asyncio
|
||||
from playwright.async_api import Page, expect
|
||||
from sqlmodel import select
|
||||
|
||||
from wiregui.auth.passwords import hash_password
|
||||
from wiregui.db import async_session
|
||||
from wiregui.models.device import Device
|
||||
from wiregui.models.user import User
|
||||
from wiregui.utils.crypto import generate_keypair, generate_preshared_key
|
||||
from tests.e2e.conftest import (
|
||||
TEST_APP_BASE,
|
||||
TEST_EMAIL,
|
||||
TEST_PASSWORD,
|
||||
_cleanup_user_by_email,
|
||||
login,
|
||||
)
|
||||
|
||||
SECOND_USER_EMAIL = "e2e-device-user2@example.com"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def second_user(test_user):
|
||||
"""Create a second user with a device for filtering tests."""
|
||||
await _cleanup_user_by_email(SECOND_USER_EMAIL)
|
||||
|
||||
async with async_session() as session:
|
||||
user = User(
|
||||
email=SECOND_USER_EMAIL,
|
||||
password_hash=hash_password("pass12345"),
|
||||
role="unprivileged",
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
yield user
|
||||
|
||||
await _cleanup_user_by_email(SECOND_USER_EMAIL)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def devices_for_both_users(test_user, second_user):
|
||||
"""Create one device per user for table/filter tests."""
|
||||
_, pub1 = generate_keypair()
|
||||
_, pub2 = generate_keypair()
|
||||
psk1 = generate_preshared_key()
|
||||
psk2 = generate_preshared_key()
|
||||
|
||||
async with async_session() as session:
|
||||
d1 = Device(
|
||||
name="admin-laptop",
|
||||
public_key=pub1,
|
||||
preshared_key=psk1,
|
||||
ipv4="10.0.0.10",
|
||||
user_id=test_user.id,
|
||||
)
|
||||
d2 = Device(
|
||||
name="user2-phone",
|
||||
public_key=pub2,
|
||||
preshared_key=psk2,
|
||||
ipv4="10.0.0.11",
|
||||
user_id=second_user.id,
|
||||
)
|
||||
session.add_all([d1, d2])
|
||||
await session.commit()
|
||||
|
||||
yield d1, d2
|
||||
|
||||
# Cleanup handled by user fixture cascade
|
||||
|
||||
|
||||
async def _go_to_admin_devices(page: Page):
|
||||
"""Login as admin and navigate to admin devices page."""
|
||||
await login(page)
|
||||
await expect(page.get_by_text("My Devices")).to_be_visible(timeout=10_000)
|
||||
await page.goto(f"{TEST_APP_BASE}/admin/devices")
|
||||
await expect(page.locator("role=main").get_by_text("All Devices")).to_be_visible(timeout=10_000)
|
||||
|
||||
|
||||
async def test_list_all_devices(page: Page, devices_for_both_users):
|
||||
"""Admin devices page lists devices from all users."""
|
||||
await _go_to_admin_devices(page)
|
||||
await expect(page.get_by_text("admin-laptop")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("user2-phone")).to_be_visible(timeout=5_000)
|
||||
|
||||
|
||||
async def test_filter_by_user(page: Page, second_user, devices_for_both_users):
|
||||
"""Filtering by user shows only that user's devices."""
|
||||
await _go_to_admin_devices(page)
|
||||
await expect(page.get_by_text("admin-laptop")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("user2-phone")).to_be_visible(timeout=5_000)
|
||||
|
||||
# Filter to second user
|
||||
await page.locator("label:has-text('Filter by User')").click()
|
||||
await page.get_by_role("option", name=SECOND_USER_EMAIL).click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
await expect(page.get_by_text("user2-phone")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("admin-laptop")).not_to_be_visible()
|
||||
|
||||
# Filter back to all
|
||||
await page.locator("label:has-text('Filter by User')").click()
|
||||
await page.get_by_role("option", name="All Users").click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
await expect(page.get_by_text("admin-laptop")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("user2-phone")).to_be_visible(timeout=5_000)
|
||||
|
||||
|
||||
async def test_create_device_with_defaults(page: Page, test_user):
|
||||
"""Create device with all defaults — config dialog appears."""
|
||||
await _go_to_admin_devices(page)
|
||||
await page.get_by_role("button", name="Add Device").click()
|
||||
await expect(page.get_by_text("New Device")).to_be_visible(timeout=5_000)
|
||||
|
||||
await page.locator("input[aria-label='Device Name']").fill("default-test-device")
|
||||
await page.get_by_role("button", name="Create").click()
|
||||
|
||||
# Config dialog should appear with WireGuard config
|
||||
await expect(page.get_by_text("Config for default-test-device")).to_be_visible(timeout=10_000)
|
||||
await expect(page.get_by_text("[Interface]")).to_be_visible(timeout=5_000)
|
||||
await page.get_by_role("button", name="Close").click()
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
# Device should be in the table
|
||||
await expect(page.get_by_role("cell", name="default-test-device").first).to_be_visible(timeout=5_000)
|
||||
|
||||
|
||||
async def test_create_device_with_overrides(page: Page, test_user):
|
||||
"""Create device with custom config overrides."""
|
||||
await _go_to_admin_devices(page)
|
||||
await page.get_by_role("button", name="Add Device").click()
|
||||
await expect(page.get_by_text("New Device")).to_be_visible(timeout=5_000)
|
||||
|
||||
await page.locator("input[aria-label='Device Name']").fill("custom-override-dev")
|
||||
await page.locator("input[aria-label='Description (optional)']").fill("Custom overrides test")
|
||||
|
||||
# Toggle off DNS default and set custom — Quasar switches use .q-toggle
|
||||
await page.locator(".q-toggle", has_text="Use default DNS").click()
|
||||
dns_input = page.locator("input[aria-label='DNS Servers']")
|
||||
await dns_input.clear()
|
||||
await dns_input.fill("8.8.8.8, 8.8.4.4")
|
||||
|
||||
# Toggle off MTU default and set custom
|
||||
await page.locator(".q-toggle", has_text="Use default MTU").click()
|
||||
mtu_input = page.locator("input[aria-label='MTU']")
|
||||
await mtu_input.clear()
|
||||
await mtu_input.fill("1400")
|
||||
|
||||
await page.get_by_role("button", name="Create").click()
|
||||
|
||||
await expect(page.get_by_text("Config for custom-override-dev")).to_be_visible(timeout=10_000)
|
||||
await page.get_by_role("button", name="Close").click()
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
await expect(page.get_by_role("cell", name="custom-override-dev").first).to_be_visible(timeout=5_000)
|
||||
|
||||
# Verify in DB
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Device).where(Device.name == "custom-override-dev")
|
||||
.order_by(Device.inserted_at.desc()).limit(1)
|
||||
)
|
||||
device = result.scalar_one()
|
||||
assert device.use_default_dns is False
|
||||
assert "8.8.8.8" in device.dns
|
||||
assert device.use_default_mtu is False
|
||||
assert device.mtu == 1400
|
||||
|
||||
|
||||
async def test_edit_device_name_and_description(page: Page, devices_for_both_users):
|
||||
"""Edit a device name and description via the edit dialog."""
|
||||
await _go_to_admin_devices(page)
|
||||
await expect(page.get_by_text("admin-laptop")).to_be_visible(timeout=5_000)
|
||||
|
||||
# Click edit button on admin-laptop row — Quasar slot buttons with icon
|
||||
row = page.locator("tr", has_text="admin-laptop")
|
||||
await row.locator(".q-btn").first.click()
|
||||
|
||||
await expect(page.get_by_text("Edit Device")).to_be_visible(timeout=5_000)
|
||||
|
||||
name_input = page.locator(".q-dialog input[aria-label='Device Name']")
|
||||
await name_input.clear()
|
||||
await name_input.fill("admin-laptop-renamed")
|
||||
|
||||
desc_input = page.locator(".q-dialog input[aria-label='Description']")
|
||||
await desc_input.clear()
|
||||
await desc_input.fill("Updated description")
|
||||
|
||||
await page.get_by_role("button", name="Save").click()
|
||||
await expect(page.get_by_text("Device updated")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("admin-laptop-renamed")).to_be_visible(timeout=5_000)
|
||||
|
||||
|
||||
async def test_delete_device(page: Page, test_user):
|
||||
"""Delete a device — removed from table."""
|
||||
_, pub = generate_keypair()
|
||||
async with async_session() as session:
|
||||
d = Device(
|
||||
name="delete-me-device",
|
||||
public_key=pub,
|
||||
preshared_key=generate_preshared_key(),
|
||||
ipv4="10.0.0.99",
|
||||
user_id=test_user.id,
|
||||
)
|
||||
session.add(d)
|
||||
await session.commit()
|
||||
|
||||
await _go_to_admin_devices(page)
|
||||
await expect(page.get_by_role("cell", name="delete-me-device")).to_be_visible(timeout=5_000)
|
||||
|
||||
# Click the delete (second) button in the row
|
||||
row = page.locator("tr", has_text="delete-me-device")
|
||||
await row.locator(".q-btn").nth(1).click()
|
||||
|
||||
await expect(page.get_by_text("Deleted delete-me-device")).to_be_visible(timeout=5_000)
|
||||
await page.wait_for_timeout(1000)
|
||||
await expect(page.get_by_role("cell", name="delete-me-device")).not_to_be_visible()
|
||||
|
||||
|
||||
async def test_config_dialog_shows_wg_config(page: Page, test_user):
|
||||
"""Config dialog after device creation shows valid WireGuard config."""
|
||||
await _go_to_admin_devices(page)
|
||||
await page.get_by_role("button", name="Add Device").click()
|
||||
await expect(page.get_by_text("New Device")).to_be_visible(timeout=5_000)
|
||||
|
||||
await page.locator("input[aria-label='Device Name']").fill("config-test-device")
|
||||
await page.get_by_role("button", name="Create").click()
|
||||
|
||||
await expect(page.get_by_text("Config for config-test-device")).to_be_visible(timeout=10_000)
|
||||
await expect(page.get_by_text("[Interface]")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("[Peer]")).to_be_visible(timeout=5_000)
|
||||
await expect(page.get_by_text("PrivateKey")).to_be_visible()
|
||||
await expect(page.get_by_role("button", name="Download .conf")).to_be_visible()
|
||||
|
||||
# QR code should be rendered
|
||||
await expect(page.locator(".q-dialog img")).to_be_visible(timeout=5_000)
|
||||
Loading…
Add table
Add a link
Reference in a new issue