feat: IdP provisioning from YAML file + Playwright e2e tests
Some checks failed
CI / test (push) Failing after 1m52s
CI / release (push) Has been skipped
CI / docker (push) Has been skipped

Add WG_IDP_CONFIG_FILE env var to seed OIDC/SAML identity providers
from a YAML file at startup, enabling GitOps and IaC workflows.
Providers are upserted by id (merge strategy preserves manual additions).

Convert all e2e tests from NiceGUI User fixture to Playwright async API
with --headed and --slowmo flags for visual debugging. Add full OIDC
login flow test against the mock-oidc service.
This commit is contained in:
Stefano Bertelli 2026-03-31 14:23:31 -05:00
parent c9ef58a244
commit 3bf6fabcff
13 changed files with 940 additions and 332 deletions

63
tests/e2e/test_login.py Normal file
View file

@ -0,0 +1,63 @@
"""End-to-end tests for login, logout, and auth guard flows."""
from playwright.async_api import Page, expect
from wiregui.db import async_session
from wiregui.models.user import User as UserModel
from wiregui.utils.time import utcnow
from tests.e2e.conftest import TEST_APP_BASE, TEST_EMAIL, TEST_PASSWORD, login
async def test_login_valid_credentials(page: Page, test_user: UserModel):
"""Valid login redirects to devices page."""
await login(page)
await expect(page.get_by_text("My Devices")).to_be_visible(timeout=10_000)
async def test_login_invalid_password(page: Page, test_user: UserModel):
"""Wrong password shows error and stays on login page."""
await login(page, password="wrongpassword")
await expect(page.get_by_text("Invalid email or password")).to_be_visible(timeout=10_000)
await expect(page.get_by_role("button", name="Sign in", exact=True)).to_be_visible()
async def test_login_nonexistent_email(page: Page, test_user: UserModel):
"""Nonexistent email shows error."""
await login(page, email="nobody@nowhere.com")
await expect(page.get_by_text("Invalid email or password")).to_be_visible(timeout=10_000)
await expect(page.get_by_role("button", name="Sign in", exact=True)).to_be_visible()
async def test_login_disabled_user(page: Page, test_user: UserModel):
"""Disabled user cannot log in."""
async with async_session() as session:
u = await session.get(UserModel, test_user.id)
u.disabled_at = utcnow()
session.add(u)
await session.commit()
try:
await login(page)
await expect(page.get_by_text("Invalid email or password")).to_be_visible(timeout=10_000)
await expect(page.get_by_role("button", name="Sign in", exact=True)).to_be_visible()
finally:
async with async_session() as session:
u = await session.get(UserModel, test_user.id)
u.disabled_at = None
session.add(u)
await session.commit()
async def test_logout(page: Page, test_user: UserModel):
"""Logout clears session and redirects to login."""
await login(page)
await expect(page.get_by_text("My Devices")).to_be_visible(timeout=10_000)
await page.get_by_text("Logout").click()
await expect(page.get_by_role("button", name="Sign in", exact=True)).to_be_visible(timeout=10_000)
async def test_unauthenticated_redirect(page: Page, test_user: UserModel):
"""Accessing a protected page without auth redirects to login."""
await page.goto(f"{TEST_APP_BASE}/devices")
await expect(page.get_by_role("button", name="Sign in", exact=True)).to_be_visible(timeout=10_000)